From e33523c08f8e7276aeb3af2c25b0c7c75f67bd67 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 2 Jul 2019 16:57:59 -0400 Subject: [PATCH 01/34] Editor: Implement meta as custom source Co-Authored-By: Riad Benguella --- .../src/components/block-list/block.js | 40 +--------- .../src/components/provider/README.md | 47 ++++++++++++ .../src/components/provider/index.js | 16 ++++ packages/block-editor/src/store/reducer.js | 76 ++++++++++--------- packages/block-editor/src/store/selectors.js | 70 +++-------------- .../block-editor/src/store/test/selectors.js | 54 ------------- .../editor/src/components/provider/index.js | 20 +---- packages/editor/src/store/actions.js | 39 +++++++++- .../editor/src/store/block-sources/README.md | 22 ++++++ .../editor/src/store/block-sources/index.js | 6 ++ .../editor/src/store/block-sources/meta.js | 51 +++++++++++++ packages/editor/src/store/test/actions.js | 8 +- 12 files changed, 238 insertions(+), 211 deletions(-) create mode 100644 packages/block-editor/src/components/provider/README.md create mode 100644 packages/editor/src/store/block-sources/README.md create mode 100644 packages/editor/src/store/block-sources/index.js create mode 100644 packages/editor/src/store/block-sources/meta.js diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 3556b829ba188..320f63555946c 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { get, reduce, size, first, last } from 'lodash'; +import { first, last } from 'lodash'; import { animated } from 'react-spring/web.cjs'; /** @@ -658,42 +658,8 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { return { setAttributes( newAttributes ) { - const { name, clientId } = ownProps; - const type = getBlockType( name ); - - function isMetaAttribute( key ) { - return get( type, [ 'attributes', key, 'source' ] ) === 'meta'; - } - - // Partition new attributes to delegate update behavior by source. - // - // TODO: A consolidated approach to external attributes sourcing - // should be devised to avoid specific handling for meta, enable - // additional attributes sources. - // - // See: https://github.com/WordPress/gutenberg/issues/2759 - const { - blockAttributes, - metaAttributes, - } = reduce( newAttributes, ( result, value, key ) => { - if ( isMetaAttribute( key ) ) { - result.metaAttributes[ type.attributes[ key ].meta ] = value; - } else { - result.blockAttributes[ key ] = value; - } - - return result; - }, { blockAttributes: {}, metaAttributes: {} } ); - - if ( size( blockAttributes ) ) { - updateBlockAttributes( clientId, blockAttributes ); - } - - if ( size( metaAttributes ) ) { - const { getSettings } = select( 'core/block-editor' ); - const onChangeMeta = getSettings().__experimentalMetaSource.onChange; - onChangeMeta( metaAttributes ); - } + const { clientId } = ownProps; + updateBlockAttributes( clientId, newAttributes ); }, onSelect( clientId = ownProps.clientId, initialPosition ) { selectBlock( clientId, initialPosition ); diff --git a/packages/block-editor/src/components/provider/README.md b/packages/block-editor/src/components/provider/README.md new file mode 100644 index 0000000000000..aac4c3fe9406b --- /dev/null +++ b/packages/block-editor/src/components/provider/README.md @@ -0,0 +1,47 @@ +BlockEditorProvider +=================== + +BlockEditorProvider is a component which establishes a new block editing context, and serves as the entry point for a new block editor. It is implemented as a [controlled input](https://reactjs.org/docs/forms.html#controlled-components), expected to receive a value of a blocks array, calling `onChange` and/or `onInput` when the user interacts to change blocks in the editor. It is intended to be used as a wrapper component, where its children comprise the user interface through which a user modifies the blocks value, notably via other components made available from this `block-editor` module. + +## Props + +### `value` + +* **Type:** `Array` +* **Required** `no` + +The current array of blocks. + +### `onChange` + +* **Type:** `Function` +* **Required** `no` + +A callback invoked when the blocks have been modified in a persistent manner. Contrasted with `onInput`, a "persistent" change is one which is not an extension of a composed input. Any update to a distinct block or block attribute is treated as persistent. + +The distinction between these two callbacks is akin to the [differences between `input` and `change` events](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event) in the DOM API: + +>The input event is fired every time the value of the element changes. **This is unlike the change event, which only fires when the value is committed**, such as by pressing the enter key, selecting a value from a list of options, and the like. + +In the context of an editor, an example usage of this distinction is for managing a history of blocks values (an "Undo"/"Redo" mechanism). While value updates should always be reflected immediately (`onInput`), you may only want history entries to reflect change milestones (`onChange`). + +### `onInput` + +* **Type:** `Function` +* **Required** `no` + +A callback invoked when the blocks have been modified in a non-persistent manner. Contrasted with `onChange`, a "non-persistent" change is one which is part of a composed input. Any sequence of updates to the same block attribute are treated as non-persistent, except for the first. + +### `onBlockAttributesChange` + +* **Type:** `Function` +* **Required** `no` + +A callback to be invoked when a single block's attributes have been updated. The callback is passed the block's `clientId` and an `attributes` object containing only the updated properties of the attributes. The callback is invoked immediately before `onChange` or `onInput`. + +### `children` + +* **Type:** `WPElement` +* **Required** `no` + +Children elements for which the BlockEditorProvider context should apply. diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 6d06246ca9170..5b7ef02ee53b7 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -68,19 +68,25 @@ class BlockEditorProvider extends Component { const { getBlocks, isLastBlockChangePersistent, + getLastBlockAttributesChange, __unstableIsLastBlockChangeIgnored, } = registry.select( 'core/block-editor' ); let blocks = getBlocks(); let isPersistent = isLastBlockChangePersistent(); + let lastBlockAttributesChange = getLastBlockAttributesChange(); this.unsubscribe = registry.subscribe( () => { const { onChange, onInput, + onBlockAttributesChange, } = this.props; + const newBlocks = getBlocks(); const newIsPersistent = isLastBlockChangePersistent(); + const newLastBlockAttributesChange = getLastBlockAttributesChange(); + if ( newBlocks !== blocks && ( this.isSyncingIncomingValue || @@ -90,9 +96,19 @@ class BlockEditorProvider extends Component { this.isSyncingIncomingValue = false; blocks = newBlocks; isPersistent = newIsPersistent; + lastBlockAttributesChange = newLastBlockAttributesChange; return; } + if ( newLastBlockAttributesChange !== lastBlockAttributesChange ) { + lastBlockAttributesChange = newLastBlockAttributesChange; + + if ( onBlockAttributesChange ) { + const [ clientId, attributes ] = newLastBlockAttributesChange; + onBlockAttributesChange( clientId, attributes ); + } + } + if ( newBlocks !== blocks || // This happens when a previous input is explicitely marked as persistent. diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index b4cd52a08886e..64c2d0802db04 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -195,29 +195,6 @@ export function isUpdatingSameBlockAttribute( action, lastAction ) { ); } -/** - * Higher-order reducer intended to reset the cache key of all blocks - * whenever the post meta values change. - * - * @param {Function} reducer Original reducer function. - * - * @return {Function} Enhanced reducer function. - */ -const withPostMetaUpdateCacheReset = ( reducer ) => ( state, action ) => { - const newState = reducer( state, action ); - const previousMetaValues = get( state, [ 'settings', '__experimentalMetaSource', 'value' ] ); - const nextMetaValues = get( newState.settings.__experimentalMetaSource, [ 'value' ] ); - // If post meta values change, reset the cache key for all blocks - if ( previousMetaValues !== nextMetaValues ) { - newState.blocks = { - ...newState.blocks, - cache: mapValues( newState.blocks.cache, () => ( {} ) ), - }; - } - - return newState; -}; - /** * Utility returning an object with an empty object value for each key. * @@ -1228,17 +1205,42 @@ export const blockListSettings = ( state = {}, action ) => { return state; }; -export default withPostMetaUpdateCacheReset( - combineReducers( { - blocks, - isTyping, - isCaretWithinFormattedText, - blockSelection, - blocksMode, - blockListSettings, - insertionPoint, - template, - settings, - preferences, - } ) -); +/** + * Reducer return an updated state representing the most recent block attribute + * update. The state is structured as a tuple of the clientId of the block and + * the partial object of updated attributes values. + * + * @param {[string,Object]} state Current state. + * @param {Object} action Action object. + * + * @return {[string,Object]} Updated state. + */ +export function lastBlockAttributesChange( state = [ null, null ], action ) { + switch ( action.type ) { + case 'UPDATE_BLOCK': + if ( ! action.updates.attributes ) { + break; + } + + return [ action.clientId, action.updates.attributes ]; + + case 'UPDATE_BLOCK_ATTRIBUTES': + return [ action.clientId, action.attributes ]; + } + + return state; +} + +export default combineReducers( { + blocks, + isTyping, + isCaretWithinFormattedText, + blockSelection, + blocksMode, + blockListSettings, + insertionPoint, + template, + settings, + preferences, + lastBlockAttributesChange, +} ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 85a7442cd8613..8acd0fe895ac9 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -64,14 +64,6 @@ const MILLISECONDS_PER_WEEK = 7 * 24 * 3600 * 1000; */ const EMPTY_ARRAY = []; -/** - * Shared reference to an empty object for cases where it is important to avoid - * returning a new object reference on every invocation. - * - * @type {Object} - */ -const EMPTY_OBJECT = {}; - /** * Returns a block's name given its client ID, or null if no block exists with * the client ID. @@ -108,42 +100,14 @@ export function isBlockValid( state, clientId ) { * * @return {Object?} Block attributes. */ -export const getBlockAttributes = createSelector( - ( state, clientId ) => { - const block = state.blocks.byClientId[ clientId ]; - if ( ! block ) { - return null; - } - - let attributes = state.blocks.attributes[ clientId ]; - - // Inject custom source attribute values. - // - // TODO: Create generic external sourcing pattern, not explicitly - // targeting meta attributes. - const type = getBlockType( block.name ); - if ( type ) { - attributes = reduce( type.attributes, ( result, value, key ) => { - if ( value.source === 'meta' ) { - if ( result === attributes ) { - result = { ...result }; - } - - result[ key ] = getPostMeta( state, value.meta ); - } - - return result; - }, attributes ); - } +export function getBlockAttributes( state, clientId ) { + const block = state.blocks.byClientId[ clientId ]; + if ( ! block ) { + return null; + } - return attributes; - }, - ( state, clientId ) => [ - state.blocks.byClientId[ clientId ], - state.blocks.attributes[ clientId ], - getPostMeta( state ), - ] -); + return state.blocks.attributes[ clientId ]; +} /** * Returns a block given its client ID. This is a parsed copy of the block, @@ -193,7 +157,7 @@ export const __unstableGetBlockWithoutInnerBlocks = createSelector( }, ( state, clientId ) => [ state.blocks.byClientId[ clientId ], - ...getBlockAttributes.getDependants( state, clientId ), + state.blocks.attributes[ clientId ], ] ); @@ -296,7 +260,6 @@ export const getBlocksByClientId = createSelector( ( clientId ) => getBlock( state, clientId ) ), ( state ) => [ - getPostMeta( state ), state.blocks.byClientId, state.blocks.order, state.blocks.attributes, @@ -656,7 +619,6 @@ export const getMultiSelectedBlocks = createSelector( state.blocks.byClientId, state.blocks.order, state.blocks.attributes, - getPostMeta( state ), ] ); @@ -1420,20 +1382,8 @@ export function __unstableIsLastBlockChangeIgnored( state ) { return state.blocks.isIgnoredChange; } -/** - * Returns the value of a post meta from the editor settings. - * - * @param {Object} state Global application state. - * @param {string} key Meta Key to retrieve - * - * @return {*} Meta value - */ -function getPostMeta( state, key ) { - if ( key === undefined ) { - return get( state, [ 'settings', '__experimentalMetaSource', 'value' ], EMPTY_OBJECT ); - } - - return get( state, [ 'settings', '__experimentalMetaSource', 'value', key ] ); +export function getLastBlockAttributesChange( state ) { + return state.lastBlockAttributesChange; } /** diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 7bc0390317ece..c3cad81dfc3b3 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -263,60 +263,6 @@ describe( 'selectors', () => { } ], } ); } ); - - it( 'should merge meta attributes for the block', () => { - registerBlockType( 'core/meta-block', { - save: ( props ) => props.attributes.text, - category: 'common', - title: 'test block', - attributes: { - foo: { - type: 'string', - source: 'meta', - meta: 'foo', - }, - }, - } ); - - const state = { - settings: { - __experimentalMetaSource: { - value: { - foo: 'bar', - }, - }, - }, - blocks: { - byClientId: { - 123: { clientId: 123, name: 'core/meta-block' }, - }, - attributes: { - 123: {}, - }, - order: { - '': [ 123 ], - 123: [], - }, - parents: { - 123: '', - }, - cache: { - 123: {}, - }, - }, - }; - - expect( getBlock( state, 123 ) ).toEqual( { - clientId: 123, - name: 'core/meta-block', - attributes: { - foo: 'bar', - }, - innerBlocks: [], - } ); - - unregisterBlockType( 'core/meta-block' ); - } ); } ); describe( 'getBlocks', () => { diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index d241aedac6fd5..bfc49d2551d83 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -74,8 +74,6 @@ class EditorProvider extends Component { getBlockEditorSettings( settings, - meta, - onMetaChange, reusableBlocks, hasUploadPermissions, canUserUseUnfilteredHTML @@ -102,10 +100,6 @@ class EditorProvider extends Component { 'templateLock', 'titlePlaceholder', ] ), - __experimentalMetaSource: { - value: meta, - onChange: onMetaChange, - }, __experimentalReusableBlocks: reusableBlocks, __experimentalMediaUpload: hasUploadPermissions ? mediaUpload : undefined, __experimentalFetchLinkSuggestions: fetchLinkSuggestions, @@ -143,10 +137,9 @@ class EditorProvider extends Component { children, blocks, resetEditorBlocks, + updateBlockSources, isReady, settings, - meta, - onMetaChange, reusableBlocks, resetEditorBlocksWithoutUndoLevel, hasUploadPermissions, @@ -158,8 +151,6 @@ class EditorProvider extends Component { const editorSettings = this.getBlockEditorSettings( settings, - meta, - onMetaChange, reusableBlocks, hasUploadPermissions, canUserUseUnfilteredHTML @@ -170,6 +161,7 @@ class EditorProvider extends Component { value={ blocks } onInput={ resetEditorBlocksWithoutUndoLevel } onChange={ resetEditorBlocks } + onBlockAttributesChange={ updateBlockSources } settings={ editorSettings } useSubRegistry={ false } > @@ -188,7 +180,6 @@ export default compose( [ canUserUseUnfilteredHTML, __unstableIsEditorReady: isEditorReady, getEditorBlocks, - getEditedPostAttribute, __experimentalGetReusableBlocks, } = select( 'core/editor' ); const { canUser } = select( 'core' ); @@ -197,7 +188,6 @@ export default compose( [ canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), isReady: isEditorReady(), blocks: getEditorBlocks(), - meta: getEditedPostAttribute( 'meta' ), reusableBlocks: __experimentalGetReusableBlocks(), hasUploadPermissions: defaultTo( canUser( 'create', 'media' ), true ), }; @@ -207,8 +197,8 @@ export default compose( [ setupEditor, updatePostLock, resetEditorBlocks, - editPost, updateEditorSettings, + updateBlockSources, } = dispatch( 'core/editor' ); const { createWarningNotice } = dispatch( 'core/notices' ); @@ -218,14 +208,12 @@ export default compose( [ createWarningNotice, resetEditorBlocks, updateEditorSettings, + updateBlockSources, resetEditorBlocksWithoutUndoLevel( blocks ) { resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false, } ); }, - onMetaChange( meta ) { - editPost( { meta } ); - }, }; } ), ] )( EditorProvider ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 073887663bd1b..7a949ee763f93 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { castArray, pick, mapValues, has } from 'lodash'; +import { find, castArray, pick, mapValues, has } from 'lodash'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; /** @@ -32,6 +32,7 @@ import { getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; +import * as sources from './block-sources'; /** * Returns an action generator used in signalling that editor has initialized with @@ -67,8 +68,28 @@ export function* setupEditor( post, edits, template ) { blocks = synchronizeBlocksWithTemplate( blocks, template ); } - yield resetEditorBlocks( blocks ); yield setupEditorState( post ); + yield resetEditorBlocks( blocks ); +} + +/** + * Action generator function used in signalling that sources are to be updated + * in response to a single block attributes update. + * + * @param {string} clientId Client ID of updated block. + * @param {Object} attributes Object of updated attributes values. + * + * @yield {Object} Yielded action objects. + */ +export function* updateBlockSources( clientId, attributes ) { + const block = find( yield select( 'core/editor', 'getEditorBlocks' ), { clientId } ); + const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); + + for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { + if ( attributes.hasOwnProperty( attributeName ) && sources[ schema.source ] ) { + yield* sources[ schema.source ].update( schema, attributes[ attributeName ] ); + } + } } /** @@ -730,7 +751,19 @@ export function unlockPostSaving( lockName ) { * * @return {Object} Action object */ -export function resetEditorBlocks( blocks, options = {} ) { +export function* resetEditorBlocks( blocks, options = {} ) { + for ( const source in Object.values( sources ) ) { + if ( typeof source.applyAll === 'function' ) { + blocks = yield* source.applyAll( blocks ); + } + + if ( typeof source.apply === 'function' ) { + for ( let i = 0; i < blocks.length; i++ ) { + blocks[ i ] = yield* source.apply( blocks[ i ] ); + } + } + } + return { type: 'RESET_EDITOR_BLOCKS', blocks, diff --git a/packages/editor/src/store/block-sources/README.md b/packages/editor/src/store/block-sources/README.md new file mode 100644 index 0000000000000..844fc670f30a4 --- /dev/null +++ b/packages/editor/src/store/block-sources/README.md @@ -0,0 +1,22 @@ +Block Sources +============= + +By default, the blocks module supports only attributes serialized into a block's comment demarcations, or those sourced from a [standard set of sources](https://developer.wordpress.org/block-editor/developers/block-api/block-attributes/). Since the blocks module is intended to be used in a number of contexts outside the post editor, the implementation of additional context-specific sources must be implemented as an external process. + +The post editor supports such additional sources for attributes (e.g. `meta` source). + +These sources are implemented here using a uniform interface for applying and responding to block updates to sourced attributes. In the future, this interface may be generalized to allow third-party extensions to either extend the post editor sources or implement their own in custom renderings of a block editor. + +## Source API + +### `apply` + +Store control called when the editor blocks value is changed. Given a block object, returns a new block with the sourced attribute value assigned. + +### `applyAll` + +Store control called when the editor blocks value is changed. Given an array of blocks, modifies block entries which source from a sourced property, responsible for returning a new block array with attribute values assigned. + +### `update` + +Store control called when a single block's attributes have been updated, before the new block value has taken effect (i.e. before `apply` and `applyAll` are once again called). Given the attribute schema and updated value, the control should reflect the update on the source. diff --git a/packages/editor/src/store/block-sources/index.js b/packages/editor/src/store/block-sources/index.js new file mode 100644 index 0000000000000..542d774c313ce --- /dev/null +++ b/packages/editor/src/store/block-sources/index.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +import * as meta from './meta'; + +export { meta }; diff --git a/packages/editor/src/store/block-sources/meta.js b/packages/editor/src/store/block-sources/meta.js new file mode 100644 index 0000000000000..15f6c9bcd13c5 --- /dev/null +++ b/packages/editor/src/store/block-sources/meta.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +import { select } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { editPost } from '../actions'; + +/** + * Store control which, given an array of blocks, modifies block entries which + * source from meta properties to assign the attribute values. + * + * @param {WPBlock[]} blocks Blocks array. + * + * @yield {Object} Yielded action objects or store controls. + * @return {WPBlock[]} Modified blocks array. + */ +export function* applyAll( blocks ) { + const meta = yield select( 'core/editor', 'getEditedPostAttribute', 'meta' ); + + for ( let i = 0; i < blocks.length; i++ ) { + const block = blocks[ i ]; + const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); + for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { + if ( schema.source === 'meta' ) { + blocks[ i ].attributes[ attributeName ] = meta[ schema.meta ]; + } + } + } + + return blocks; +} + +/** + * Store control invoked upon a block attributes update, responsible for + * reflecting an update in a meta value. + * + * @param {Object} schema Block type attribute schema. + * @param {*} value Updated block attribute value. + * + * @yield {Object} Yielded action objects or store controls. + */ +export function* update( schema, value ) { + yield editPost( { + meta: { + [ schema.meta ]: value, + }, + } ); +} diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index dd78100a4385d..f71d0e0578b4a 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -655,10 +655,6 @@ describe( 'Editor actions', () => { post: { content: { raw: '' }, status: 'publish' }, } ); } ); - it( 'should yield action object for resetEditorBlocks', () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( actions.resetEditorBlocks( [] ) ); - } ); it( 'should yield action object for setupEditorState', () => { const { value } = fulfillment.next(); expect( value ).toEqual( @@ -667,6 +663,10 @@ describe( 'Editor actions', () => { ) ); } ); + it( 'should yield action object for resetEditorBlocks', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( actions.resetEditorBlocks( [] ) ); + } ); } ); describe( 'resetPost', () => { From 81bdcb1cf9bcfe15938db9862af74d03bcf6b7df Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 5 Jul 2019 15:06:02 -0400 Subject: [PATCH 02/34] Block Editor: Skip outgoing sync next value only if matches by reference --- packages/block-editor/src/components/provider/index.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 5b7ef02ee53b7..eabd2ac2c1ebc 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -34,10 +34,10 @@ class BlockEditorProvider extends Component { this.attachChangeObserver( registry ); } - if ( this.isSyncingOutcomingValue ) { - this.isSyncingOutcomingValue = false; + if ( this.isSyncingOutcomingValue === value ) { + this.isSyncingOutcomingValue = null; } else if ( value !== prevProps.value ) { - this.isSyncingIncomingValue = true; + this.isSyncingIncomingValue = value; resetBlocks( value ); } } @@ -93,7 +93,7 @@ class BlockEditorProvider extends Component { __unstableIsLastBlockChangeIgnored() ) ) { - this.isSyncingIncomingValue = false; + this.isSyncingIncomingValue = null; blocks = newBlocks; isPersistent = newIsPersistent; lastBlockAttributesChange = newLastBlockAttributesChange; @@ -117,7 +117,7 @@ class BlockEditorProvider extends Component { // When knowing the blocks value is changing, assign instance // value to skip reset in subsequent `componentDidUpdate`. if ( newBlocks !== blocks ) { - this.isSyncingOutcomingValue = true; + this.isSyncingOutcomingValue = newBlocks; } blocks = newBlocks; From 6f58b3e31a010ff2b921d553502437f9d4ff4771 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 5 Jul 2019 15:32:22 -0400 Subject: [PATCH 03/34] Editor: Implement editor tearDown to un-ready state --- packages/editor/src/components/provider/index.js | 6 ++++++ packages/editor/src/store/actions.js | 10 ++++++++++ packages/editor/src/store/reducer.js | 3 +++ 3 files changed, 19 insertions(+) diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index bfc49d2551d83..cf454fd887830 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -131,6 +131,10 @@ class EditorProvider extends Component { } } + componentWillUnmount() { + this.props.tearDownEditor(); + } + render() { const { canUserUseUnfilteredHTML, @@ -199,6 +203,7 @@ export default compose( [ resetEditorBlocks, updateEditorSettings, updateBlockSources, + tearDownEditor, } = dispatch( 'core/editor' ); const { createWarningNotice } = dispatch( 'core/notices' ); @@ -209,6 +214,7 @@ export default compose( [ resetEditorBlocks, updateEditorSettings, updateBlockSources, + tearDownEditor, resetEditorBlocksWithoutUndoLevel( blocks ) { resetEditorBlocks( blocks, { __unstableShouldCreateUndoLevel: false, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 7a949ee763f93..3406da99f91d7 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -72,6 +72,16 @@ export function* setupEditor( post, edits, template ) { yield resetEditorBlocks( blocks ); } +/** + * Returns an action object signalling that the editor is being destroyed and + * that any necessary state or side-effect cleanup should occur. + * + * @return {Object} Action object. + */ +export function tearDownEditor() { + return { type: 'TEAR_DOWN_EDITOR' }; +} + /** * Action generator function used in signalling that sources are to be updated * in response to a single block attributes update. diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index a6f2bd4a4565a..6676b74dcbc39 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -558,6 +558,9 @@ export function isReady( state = false, action ) { switch ( action.type ) { case 'SETUP_EDITOR_STATE': return true; + + case 'TEAR_DOWN_EDITOR': + return false; } return state; From df30e1819224bb9f763dd22624faa54cf79fd477 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 5 Jul 2019 15:34:04 -0400 Subject: [PATCH 04/34] Editor: Add subscription action generator for managing sources dependencies --- packages/editor/src/store/actions.js | 113 ++++++++++++++++-- .../editor/src/store/block-sources/meta.js | 41 ++++--- packages/editor/src/store/controls.js | 27 +++++ packages/editor/src/store/index.js | 8 +- 4 files changed, 155 insertions(+), 34 deletions(-) create mode 100644 packages/editor/src/store/controls.js diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 3406da99f91d7..d8fb777a9cb59 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -13,6 +13,7 @@ import { parse, synchronizeBlocksWithTemplate, } from '@wordpress/blocks'; +import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies @@ -32,8 +33,61 @@ import { getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; +import { awaitNextStateChange } from './controls'; import * as sources from './block-sources'; +/** + * Given a blocks array, returns a blocks array with sourced attribute values + * applied. The reference will remain consistent with the original argument if + * no attribute values must be overridden. If sourced values are applied, the + * return value will be a modified copy of the original array. + * + * @param {WPBlock[]} blocks Original blocks array. + * + * @return {WPBlock[]} Blocks array with sourced values applied. + */ +function* getBlocksWithSourcedAttributes( blocks ) { + let workingBlocks = blocks; + for ( let i = 0; i < blocks.length; i++ ) { + const block = blocks[ i ]; + const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); + + for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { + if ( ! sources[ schema.source ] || ! sources[ schema.source ].apply ) { + continue; + } + + // TODO: This should ideally leverage the `subscribeSources` cache, + // or at worst at least be cached here one-per-source. + const dependencies = yield* sources[ schema.source ].getDependencies(); + + const sourcedAttributeValue = sources[ schema.source ].apply( schema, dependencies ); + + // It's only necessary to apply the value if it differs from the + // block's locally-assigned value, to avoid needlessly resetting + // the block editor. + if ( sourcedAttributeValue === block.attributes[ attributeName ] ) { + continue; + } + + // Create a shallow clone to mutate, leaving the original intact. + if ( workingBlocks === blocks ) { + workingBlocks = [ ...workingBlocks ]; + } + + workingBlocks.splice( i, 1, { + ...block, + attributes: { + ...block.attributes, + [ attributeName ]: sourcedAttributeValue, + }, + } ); + } + } + + return workingBlocks; +} + /** * Returns an action generator used in signalling that editor has initialized with * the specified post object and editor settings. @@ -70,6 +124,7 @@ export function* setupEditor( post, edits, template ) { yield setupEditorState( post ); yield resetEditorBlocks( blocks ); + yield* subscribeSources(); } /** @@ -82,6 +137,50 @@ export function tearDownEditor() { return { type: 'TEAR_DOWN_EDITOR' }; } +/** + * Returns an action generator which loops to await the next state change, + * calling to reset blocks when a block source dependencies change. + * + * @yield {Object} Action object. + */ +export const subscribeSources = ( () => { + const lastDependencies = new WeakMap(); + + return function* () { + while ( true ) { + yield awaitNextStateChange(); + + // The bailout case: If the editor becomes unmounted, it will flag + // itself as non-ready. Effectively unsubscribes from the registry. + const isStillReady = yield select( 'core/editor', '__unstableIsEditorReady' ); + if ( ! isStillReady ) { + break; + } + + let reset = false; + for ( const source of Object.values( sources ) ) { + if ( ! source.getDependencies ) { + continue; + } + + const dependencies = yield* source.getDependencies(); + if ( ! isShallowEqual( dependencies, lastDependencies.get( source ) ) ) { + // Allow the loop to continue in order to assign latest + // dependencies values, but mark for reset. Avoid reset + // from the first assignment. + reset = reset || lastDependencies.has( source ); + + lastDependencies.set( source, dependencies ); + } + } + + if ( reset ) { + yield resetEditorBlocks( yield select( 'core/editor', 'getEditorBlocks' ) ); + } + } + }; +} )(); + /** * Action generator function used in signalling that sources are to be updated * in response to a single block attributes update. @@ -762,21 +861,9 @@ export function unlockPostSaving( lockName ) { * @return {Object} Action object */ export function* resetEditorBlocks( blocks, options = {} ) { - for ( const source in Object.values( sources ) ) { - if ( typeof source.applyAll === 'function' ) { - blocks = yield* source.applyAll( blocks ); - } - - if ( typeof source.apply === 'function' ) { - for ( let i = 0; i < blocks.length; i++ ) { - blocks[ i ] = yield* source.apply( blocks[ i ] ); - } - } - } - return { type: 'RESET_EDITOR_BLOCKS', - blocks, + blocks: yield* getBlocksWithSourcedAttributes( blocks ), shouldCreateUndoLevel: options.__unstableShouldCreateUndoLevel !== false, }; } diff --git a/packages/editor/src/store/block-sources/meta.js b/packages/editor/src/store/block-sources/meta.js index 15f6c9bcd13c5..4ae9434a1d3aa 100644 --- a/packages/editor/src/store/block-sources/meta.js +++ b/packages/editor/src/store/block-sources/meta.js @@ -9,28 +9,31 @@ import { select } from '@wordpress/data-controls'; import { editPost } from '../actions'; /** - * Store control which, given an array of blocks, modifies block entries which - * source from meta properties to assign the attribute values. + * Store control invoked upon a state changes, responsible for returning an + * object of dependencies. When a change in dependencies occurs (by shallow + * equality of the returned object), blocks are reset to apply the new sourced + * value. * - * @param {WPBlock[]} blocks Blocks array. - * - * @yield {Object} Yielded action objects or store controls. - * @return {WPBlock[]} Modified blocks array. + * @yield {Object} Optional yielded controls. + * @return {Object} Dependencies as object. */ -export function* applyAll( blocks ) { - const meta = yield select( 'core/editor', 'getEditedPostAttribute', 'meta' ); - - for ( let i = 0; i < blocks.length; i++ ) { - const block = blocks[ i ]; - const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); - for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { - if ( schema.source === 'meta' ) { - blocks[ i ].attributes[ attributeName ] = meta[ schema.meta ]; - } - } - } +export function* getDependencies() { + return { + meta: yield select( 'core/editor', 'getEditedPostAttribute', 'meta' ), + }; +} - return blocks; +/** + * Given an attribute schema and dependencies data, returns a source value. + * + * @param {Object} schema Block type attribute schema. + * @param {Object} dependencies Source dependencies. + * @param {Object} dependencies.meta Post meta. + * + * @return {Object} Block attribute value. + */ +export function apply( schema, { meta } ) { + return meta[ schema.meta ]; } /** diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js new file mode 100644 index 0000000000000..726f214684bb1 --- /dev/null +++ b/packages/editor/src/store/controls.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { createRegistryControl } from '@wordpress/data'; + +/** + * Returns an control descriptor signalling to subscribe to the registry and + * resolve the control promise only when the next state change occurs. + * + * @return {Object} Control descriptor. + */ +export function awaitNextStateChange() { + return { type: 'AWAIT_NEXT_STATE_CHANGE' }; +} + +const controls = { + AWAIT_NEXT_STATE_CHANGE: createRegistryControl( + ( registry ) => () => new Promise( ( resolve ) => { + const unsubscribe = registry.subscribe( () => { + unsubscribe(); + resolve(); + } ); + } ) + ), +}; + +export default controls; diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index 33c5686396097..8f377151e08d4 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { registerStore } from '@wordpress/data'; -import { controls } from '@wordpress/data-controls'; +import { controls as dataControls } from '@wordpress/data-controls'; /** * Internal dependencies @@ -11,6 +11,7 @@ import reducer from './reducer'; import applyMiddlewares from './middlewares'; import * as selectors from './selectors'; import * as actions from './actions'; +import controls from './controls'; import { STORE_KEY } from './constants'; /** @@ -24,7 +25,10 @@ export const storeConfig = { reducer, selectors, actions, - controls, + controls: { + ...dataControls, + ...controls, + }, }; const store = registerStore( STORE_KEY, { From 6bfb9b0324db427bf244bc179ffeac1e4f4213bc Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 11:29:55 -0400 Subject: [PATCH 05/34] Editor: Track source dependencies by registry --- packages/editor/src/store/actions.js | 69 ++++++++++++++++----------- packages/editor/src/store/controls.js | 2 +- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index d8fb777a9cb59..bbf269a37de70 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -36,6 +36,13 @@ import { import { awaitNextStateChange } from './controls'; import * as sources from './block-sources'; +/** + * Map of Registry instance to WeakMap of dependencies by custom source. + * + * @type WeakMap> + */ +const lastBlockSourceDependenciesByRegistry = new WeakMap; + /** * Given a blocks array, returns a blocks array with sourced attribute values * applied. The reference will remain consistent with the original argument if @@ -143,43 +150,47 @@ export function tearDownEditor() { * * @yield {Object} Action object. */ -export const subscribeSources = ( () => { - const lastDependencies = new WeakMap(); +export function* subscribeSources() { + while ( true ) { + const registry = yield awaitNextStateChange(); - return function* () { - while ( true ) { - yield awaitNextStateChange(); + // The bailout case: If the editor becomes unmounted, it will flag + // itself as non-ready. Effectively unsubscribes from the registry. + const isStillReady = yield select( 'core/editor', '__unstableIsEditorReady' ); + if ( ! isStillReady ) { + break; + } - // The bailout case: If the editor becomes unmounted, it will flag - // itself as non-ready. Effectively unsubscribes from the registry. - const isStillReady = yield select( 'core/editor', '__unstableIsEditorReady' ); - if ( ! isStillReady ) { - break; + let reset = false; + for ( const source of Object.values( sources ) ) { + if ( ! source.getDependencies ) { + continue; } - let reset = false; - for ( const source of Object.values( sources ) ) { - if ( ! source.getDependencies ) { - continue; - } - - const dependencies = yield* source.getDependencies(); - if ( ! isShallowEqual( dependencies, lastDependencies.get( source ) ) ) { - // Allow the loop to continue in order to assign latest - // dependencies values, but mark for reset. Avoid reset - // from the first assignment. - reset = reset || lastDependencies.has( source ); - - lastDependencies.set( source, dependencies ); - } + const dependencies = yield* source.getDependencies(); + + if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) { + lastBlockSourceDependenciesByRegistry.set( registry, new WeakMap ); } - if ( reset ) { - yield resetEditorBlocks( yield select( 'core/editor', 'getEditorBlocks' ) ); + const lastBlockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry ); + const lastDependencies = lastBlockSourceDependencies.get( source ); + + if ( ! isShallowEqual( dependencies, lastDependencies ) ) { + // Allow the loop to continue in order to assign latest + // dependencies values, but mark for reset. Avoid reset + // from the first assignment. + reset = reset || lastDependencies !== undefined; + + lastBlockSourceDependencies.set( source, dependencies ); } } - }; -} )(); + + if ( reset ) { + yield resetEditorBlocks( yield select( 'core/editor', 'getEditorBlocks' ) ); + } + } +} /** * Action generator function used in signalling that sources are to be updated diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index 726f214684bb1..73ff5899a8490 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -18,7 +18,7 @@ const controls = { ( registry ) => () => new Promise( ( resolve ) => { const unsubscribe = registry.subscribe( () => { unsubscribe(); - resolve(); + resolve( registry ); } ); } ) ), From 2a6ef11644d33bbe0b6af822a45a865e9aa9ff2f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 11:59:58 -0400 Subject: [PATCH 06/34] Editor: Prepare initial blocks reset for applied post-sourced values --- packages/editor/src/store/actions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index bbf269a37de70..2360bfb4b20b6 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -129,8 +129,9 @@ export function* setupEditor( post, edits, template ) { blocks = synchronizeBlocksWithTemplate( blocks, template ); } - yield setupEditorState( post ); + yield resetPost( post ); yield resetEditorBlocks( blocks ); + yield setupEditorState( post ); yield* subscribeSources(); } From 9b99c78ddf6df4b5c4f09fc333798405ddcf5af7 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 13:57:37 -0400 Subject: [PATCH 07/34] Editor: Yield editor setup action after state preparation Ensure initial edits set to expected values, since preceding resetPost would unset them otherwise if already assigned --- packages/editor/src/store/actions.js | 13 +++++------ packages/editor/src/store/test/actions.js | 27 +++++++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 2360bfb4b20b6..b69fcb56ce3bc 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -104,13 +104,6 @@ function* getBlocksWithSourcedAttributes( blocks ) { * @param {Array?} template Block Template. */ export function* setupEditor( post, edits, template ) { - yield { - type: 'SETUP_EDITOR', - post, - edits, - template, - }; - // In order to ensure maximum of a single parse during setup, edits are // included as part of editor setup action. Assume edited content as // canonical if provided, falling back to post. @@ -132,6 +125,12 @@ export function* setupEditor( post, edits, template ) { yield resetPost( post ); yield resetEditorBlocks( blocks ); yield setupEditorState( post ); + yield { + type: 'SETUP_EDITOR', + post, + edits, + template, + }; yield* subscribeSources(); } diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index f71d0e0578b4a..6306a73aec055 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -640,20 +640,26 @@ describe( 'Post generator actions', () => { describe( 'Editor actions', () => { describe( 'setupEditor()', () => { + const post = { content: { raw: '' }, status: 'publish' }; + let fulfillment; - const reset = ( post, edits, template ) => fulfillment = actions + const reset = ( edits, template ) => fulfillment = actions .setupEditor( post, edits, template, ); - it( 'should yield the SETUP_EDITOR action', () => { - reset( { content: { raw: '' }, status: 'publish' } ); + beforeAll( () => { + reset(); + } ); + + it( 'should yield action object for resetPost', () => { const { value } = fulfillment.next(); - expect( value ).toEqual( { - type: 'SETUP_EDITOR', - post: { content: { raw: '' }, status: 'publish' }, - } ); + expect( value ).toEqual( actions.resetPost( post ) ); + } ); + it( 'should yield action object for resetEditorBlocks', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( actions.resetEditorBlocks( [] ) ); } ); it( 'should yield action object for setupEditorState', () => { const { value } = fulfillment.next(); @@ -663,9 +669,12 @@ describe( 'Editor actions', () => { ) ); } ); - it( 'should yield action object for resetEditorBlocks', () => { + it( 'should yield the SETUP_EDITOR action', () => { const { value } = fulfillment.next(); - expect( value ).toEqual( actions.resetEditorBlocks( [] ) ); + expect( value ).toEqual( { + type: 'SETUP_EDITOR', + post: { content: { raw: '' }, status: 'publish' }, + } ); } ); } ); From db51f778b053e0c4fe7a4c031a7d6cb0f95f4b8a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 15:16:35 -0400 Subject: [PATCH 08/34] Editor: Update block sources as part of reset step --- .../editor/src/components/provider/index.js | 65 ++++++++--- packages/editor/src/store/actions.js | 107 +++++++++++++----- .../store/block-sources/__mocks__/index.js | 1 + packages/editor/src/store/controls.js | 11 ++ packages/editor/src/store/test/actions.js | 1 + 5 files changed, 141 insertions(+), 44 deletions(-) create mode 100644 packages/editor/src/store/block-sources/__mocks__/index.js diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index cf454fd887830..a4c444d9ba58f 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -44,6 +44,9 @@ class EditorProvider extends Component { constructor( props ) { super( ...arguments ); + this.setLastBlockAttributesChange = this.setLastBlockAttributesChange.bind( this ); + this.resetEditorBlocks = this.resetEditorBlocks.bind( this ); + this.resetEditorBlocksWithoutUndoLevel = this.resetEditorBlocksWithoutUndoLevel.bind( this ); this.getBlockEditorSettings = memize( this.getBlockEditorSettings, { maxSize: 1, } ); @@ -135,17 +138,60 @@ class EditorProvider extends Component { this.props.tearDownEditor(); } + /** + * Sets an instance property ahead of a subsequent onChange or onInput + * callback to merge into options an inference that the change resulted + * from the block change. + * + * @param {string} clientId Updated block client ID. + * @param {Object} attributes Subset of updated attributes. + */ + setLastBlockAttributesChange( clientId, attributes ) { + this.lastBlockAttributesChange = { + ...this.lastBlockAttributesChange, + [ clientId ]: attributes, + }; + } + + /** + * Handles block resetting with optional options. + * + * @param {Array} blocks New blocks value. + * @param {?Object} options Optional options. + */ + resetEditorBlocks( blocks, options ) { + if ( this.lastBlockAttributesChange ) { + options = { + ...options, + __unstableLastBlockAttributesChange: this.lastBlockAttributesChange, + }; + + delete this.lastBlockAttributesChange; + } + + this.props.resetEditorBlocks( blocks, options ); + } + + /** + * Handles block resetting with optional options, marking update as to + * avoid creation of an undo level. + * + * @param {Array} blocks New blocks value. + */ + resetEditorBlocksWithoutUndoLevel( blocks ) { + this.resetEditorBlocks( blocks, { + __unstableShouldCreateUndoLevel: true, + } ); + } + render() { const { canUserUseUnfilteredHTML, children, blocks, - resetEditorBlocks, - updateBlockSources, isReady, settings, reusableBlocks, - resetEditorBlocksWithoutUndoLevel, hasUploadPermissions, } = this.props; @@ -163,9 +209,9 @@ class EditorProvider extends Component { return ( @@ -202,7 +248,6 @@ export default compose( [ updatePostLock, resetEditorBlocks, updateEditorSettings, - updateBlockSources, tearDownEditor, } = dispatch( 'core/editor' ); const { createWarningNotice } = dispatch( 'core/notices' ); @@ -213,13 +258,7 @@ export default compose( [ createWarningNotice, resetEditorBlocks, updateEditorSettings, - updateBlockSources, tearDownEditor, - resetEditorBlocksWithoutUndoLevel( blocks ) { - resetEditorBlocks( blocks, { - __unstableShouldCreateUndoLevel: false, - } ); - }, }; } ), ] )( EditorProvider ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index b69fcb56ce3bc..fdb3d0dd6bda4 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { find, castArray, pick, mapValues, has } from 'lodash'; +import { castArray, pick, mapValues, has } from 'lodash'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; /** @@ -33,7 +33,7 @@ import { getNotificationArgumentsForSaveFail, getNotificationArgumentsForTrashFail, } from './utils/notice-builder'; -import { awaitNextStateChange } from './controls'; +import { awaitNextStateChange, getRegistry } from './controls'; import * as sources from './block-sources'; /** @@ -64,10 +64,17 @@ function* getBlocksWithSourcedAttributes( blocks ) { continue; } - // TODO: This should ideally leverage the `subscribeSources` cache, - // or at worst at least be cached here one-per-source. - const dependencies = yield* sources[ schema.source ].getDependencies(); + const registry = yield getRegistry(); + if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) { + continue; + } + + const blockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry ); + if ( ! blockSourceDependencies.has( sources[ schema.source ] ) ) { + continue; + } + const dependencies = blockSourceDependencies.get( sources[ schema.source ] ); const sourcedAttributeValue = sources[ schema.source ].apply( schema, dependencies ); // It's only necessary to apply the value if it differs from the @@ -95,6 +102,32 @@ function* getBlocksWithSourcedAttributes( blocks ) { return workingBlocks; } +/** + * Refreshes the last block source dependencies, optionally for a given subset + * of sources (defaults to the full set of sources). + * + * @param {?Array} sourcesToUpdate Optional subset of sources to reset. + * + * @yield {Object} Yielded actions or control descriptors. + */ +function* resetLastBlockSourceDependencies( sourcesToUpdate = Object.values( sources ) ) { + if ( ! sourcesToUpdate.length ) { + return; + } + + const registry = yield getRegistry(); + + for ( const source of sourcesToUpdate ) { + if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) { + lastBlockSourceDependenciesByRegistry.set( registry, new WeakMap ); + } + + const lastBlockSourceDependencies = lastBlockSourceDependenciesByRegistry.get( registry ); + const dependencies = yield* source.getDependencies(); + lastBlockSourceDependencies.set( source, dependencies ); + } +} + /** * Returns an action generator used in signalling that editor has initialized with * the specified post object and editor settings. @@ -123,6 +156,7 @@ export function* setupEditor( post, edits, template ) { } yield resetPost( post ); + yield* resetLastBlockSourceDependencies(); yield resetEditorBlocks( blocks ); yield setupEditorState( post ); yield { @@ -177,12 +211,11 @@ export function* subscribeSources() { const lastDependencies = lastBlockSourceDependencies.get( source ); if ( ! isShallowEqual( dependencies, lastDependencies ) ) { - // Allow the loop to continue in order to assign latest - // dependencies values, but mark for reset. Avoid reset - // from the first assignment. - reset = reset || lastDependencies !== undefined; - lastBlockSourceDependencies.set( source, dependencies ); + + // Allow the loop to continue in order to assign latest + // dependencies values, but mark for reset. + reset = true; } } @@ -192,26 +225,6 @@ export function* subscribeSources() { } } -/** - * Action generator function used in signalling that sources are to be updated - * in response to a single block attributes update. - * - * @param {string} clientId Client ID of updated block. - * @param {Object} attributes Object of updated attributes values. - * - * @yield {Object} Yielded action objects. - */ -export function* updateBlockSources( clientId, attributes ) { - const block = find( yield select( 'core/editor', 'getEditorBlocks' ), { clientId } ); - const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); - - for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { - if ( attributes.hasOwnProperty( attributeName ) && sources[ schema.source ] ) { - yield* sources[ schema.source ].update( schema, attributes[ attributeName ] ); - } - } -} - /** * Returns an action object used in signalling that the latest version of the * post has been received, either by initialization or save. @@ -872,10 +885,42 @@ export function unlockPostSaving( lockName ) { * @return {Object} Action object */ export function* resetEditorBlocks( blocks, options = {} ) { + const { + __unstableLastBlockAttributesChange: lastBlockAttributesChange, + __unstableShouldCreateUndoLevel: shouldCreateUndoLevel = true, + } = options; + + // Sync to sources from block attributes updates. + if ( lastBlockAttributesChange ) { + const updatedSources = new Set; + + for ( let i = 0; i < blocks.length; i++ ) { + const block = blocks[ i ]; + if ( ! lastBlockAttributesChange.hasOwnProperty( block.clientId ) ) { + continue; + } + + const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); + const changedAttributes = lastBlockAttributesChange[ block.clientId ]; + + for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { + if ( changedAttributes.hasOwnProperty( attributeName ) && sources[ schema.source ] && sources[ schema.source ].update ) { + yield* sources[ schema.source ].update( schema, changedAttributes[ attributeName ] ); + updatedSources.add( sources[ schema.source ] ); + } + } + } + + // Dependencies are reset so that source dependencies subscription + // skips a reset which would otherwise occur by dependencies change. + // This assures that at most one reset occurs per block change. + yield* resetLastBlockSourceDependencies( Array.from( updatedSources ) ); + } + return { type: 'RESET_EDITOR_BLOCKS', blocks: yield* getBlocksWithSourcedAttributes( blocks ), - shouldCreateUndoLevel: options.__unstableShouldCreateUndoLevel !== false, + shouldCreateUndoLevel, }; } diff --git a/packages/editor/src/store/block-sources/__mocks__/index.js b/packages/editor/src/store/block-sources/__mocks__/index.js new file mode 100644 index 0000000000000..cb0ff5c3b541f --- /dev/null +++ b/packages/editor/src/store/block-sources/__mocks__/index.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index 73ff5899a8490..a090bc8faef3a 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -13,6 +13,16 @@ export function awaitNextStateChange() { return { type: 'AWAIT_NEXT_STATE_CHANGE' }; } +/** + * Returns an control descriptor signalling to resolve with the current data + * registry. + * + * @return {Object} Control descriptor. + */ +export function getRegistry() { + return { type: 'GET_REGISTRY' }; +} + const controls = { AWAIT_NEXT_STATE_CHANGE: createRegistryControl( ( registry ) => () => new Promise( ( resolve ) => { @@ -22,6 +32,7 @@ const controls = { } ); } ) ), + GET_REGISTRY: createRegistryControl( ( registry ) => () => registry ), }; export default controls; diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 6306a73aec055..437bfc051d119 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -20,6 +20,7 @@ import { } from '../constants'; jest.mock( '@wordpress/data-controls' ); +jest.mock( '../block-sources' ); select.mockImplementation( ( ...args ) => { const { select: actualSelect } = jest From e1786a177b63c58362b9b198f81067260c0ca2ee Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 15:18:33 -0400 Subject: [PATCH 09/34] Editor: Retrieve registry at most once in applying sourced attributes --- packages/editor/src/store/actions.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index fdb3d0dd6bda4..28ec7fea7f057 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -54,6 +54,8 @@ const lastBlockSourceDependenciesByRegistry = new WeakMap; * @return {WPBlock[]} Blocks array with sourced values applied. */ function* getBlocksWithSourcedAttributes( blocks ) { + const registry = yield getRegistry(); + let workingBlocks = blocks; for ( let i = 0; i < blocks.length; i++ ) { const block = blocks[ i ]; @@ -64,7 +66,6 @@ function* getBlocksWithSourcedAttributes( blocks ) { continue; } - const registry = yield getRegistry(); if ( ! lastBlockSourceDependenciesByRegistry.has( registry ) ) { continue; } From c2e0c1f7987099ea6b1228b3ff2687f09ed954bb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 15:28:07 -0400 Subject: [PATCH 10/34] Editor: Account for source values apply / update in inner blocks --- packages/editor/src/store/actions.js | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 28ec7fea7f057..b986617280449 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -58,7 +58,7 @@ function* getBlocksWithSourcedAttributes( blocks ) { let workingBlocks = blocks; for ( let i = 0; i < blocks.length; i++ ) { - const block = blocks[ i ]; + let block = blocks[ i ]; const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { @@ -90,13 +90,32 @@ function* getBlocksWithSourcedAttributes( blocks ) { workingBlocks = [ ...workingBlocks ]; } - workingBlocks.splice( i, 1, { + block = { ...block, attributes: { ...block.attributes, [ attributeName ]: sourcedAttributeValue, }, - } ); + }; + + workingBlocks.splice( i, 1, block ); + } + + // Recurse to apply source attributes to inner blocks. + if ( block.innerBlocks.length ) { + const appliedInnerBlocks = yield* getBlocksWithSourcedAttributes( block.innerBlocks ); + if ( appliedInnerBlocks !== block.innerBlocks ) { + if ( workingBlocks === blocks ) { + workingBlocks = [ ...workingBlocks ]; + } + + block = { + ...block, + innerBlocks: appliedInnerBlocks, + }; + + workingBlocks.splice( i, 1, block ); + } } } @@ -895,8 +914,16 @@ export function* resetEditorBlocks( blocks, options = {} ) { if ( lastBlockAttributesChange ) { const updatedSources = new Set; - for ( let i = 0; i < blocks.length; i++ ) { - const block = blocks[ i ]; + const blockStack = [ ...blocks ]; + + for ( let i = 0; i < blockStack.length; i++ ) { + const block = blockStack[ i ]; + + // Account for changes occurring within nested block. + if ( block.innerBlocks.length ) { + blockStack.push( ...block.innerBlocks ); + } + if ( ! lastBlockAttributesChange.hasOwnProperty( block.clientId ) ) { continue; } From ba1a8cd52fc6827c215a238addba2b8823ad4a87 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 15:48:23 -0400 Subject: [PATCH 11/34] Editor: Update block sources documentation per interface changes --- packages/editor/src/store/block-sources/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/store/block-sources/README.md b/packages/editor/src/store/block-sources/README.md index 844fc670f30a4..feac1bbde5547 100644 --- a/packages/editor/src/store/block-sources/README.md +++ b/packages/editor/src/store/block-sources/README.md @@ -9,13 +9,13 @@ These sources are implemented here using a uniform interface for applying and re ## Source API -### `apply` +### `getDependencies` -Store control called when the editor blocks value is changed. Given a block object, returns a new block with the sourced attribute value assigned. +Store control called on every store change, expected to return an object whose values represent the data blocks assigned this source depend. When these values change, all blocks assigned this source are automatically updated. The value returned from this function is passed as the second argument of the source's `apply` function, where it is expected to be used as shared data relevant for sourcing the attribute value. -### `applyAll` +### `apply` -Store control called when the editor blocks value is changed. Given an array of blocks, modifies block entries which source from a sourced property, responsible for returning a new block array with attribute values assigned. +Function called to retrieve an attribute value for a block. Given the attribute schema and the dependencies defined by the source's `getDependencies`, the function should return the expected attribute value. ### `update` From f4f740352b6edc77e950b6f46418daab3af5728a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 15:54:17 -0400 Subject: [PATCH 12/34] Block Editor: Add lastBlockAttributesChange unit tests --- .../block-editor/src/store/test/reducer.js | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index 99f0ecb16282a..c143a5f8bbfa1 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -28,6 +28,7 @@ import { insertionPoint, template, blockListSettings, + lastBlockAttributesChange, } from '../reducer'; describe( 'state', () => { @@ -2390,4 +2391,60 @@ describe( 'state', () => { expect( state ).toEqual( {} ); } ); } ); + + describe( 'lastBlockAttributesChange', () => { + it( 'is a tuple, defaulting to null values', () => { + const state = lastBlockAttributesChange( undefined, {} ); + + expect( state ).toEqual( [ null, null ] ); + } ); + + it( 'does not return updated value when block update does not include attributes', () => { + const original = deepFreeze( [ null, null ] ); + + const state = lastBlockAttributesChange( original, { + type: 'UPDATE_BLOCK', + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + updates: {}, + } ); + + expect( state ).toBe( original ); + } ); + + it( 'returns updated value when block update includes attributes', () => { + const original = deepFreeze( [ null, null ] ); + + const state = lastBlockAttributesChange( original, { + type: 'UPDATE_BLOCK', + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + updates: { + attributes: { + food: 'banana', + }, + }, + } ); + + expect( state ).toEqual( [ + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + { food: 'banana' }, + ] ); + } ); + + it( 'returns updated value when explicit block attributes update', () => { + const original = deepFreeze( [ null, null ] ); + + const state = lastBlockAttributesChange( original, { + type: 'UPDATE_BLOCK_ATTRIBUTES', + clientId: 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + attributes: { + food: 'banana', + }, + } ); + + expect( state ).toEqual( [ + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', + { food: 'banana' }, + ] ); + } ); + } ); } ); From 19618fd0badb8080b3bf124f412f05b853d26a46 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Mon, 8 Jul 2019 15:58:24 -0400 Subject: [PATCH 13/34] Block Editor: Document, test getLastBlockAttributesChange selector --- packages/block-editor/src/store/selectors.js | 12 ++++++++++++ packages/block-editor/src/store/test/selectors.js | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 8acd0fe895ac9..6e890ecbce398 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1382,6 +1382,18 @@ export function __unstableIsLastBlockChangeIgnored( state ) { return state.blocks.isIgnoredChange; } +/** + * Returns the last block attributes changed. This is not guaranteed to be a + * result of the most recent action dispatched, so the onus is on the consumer + * to subscribe and compare to own local reference if only interested in the + * most recent action. + * + * @param {Object} state Block editor state. + * + * @return {[string,Object]} Most recent block attributes changed, a tuple of + * the clientId of the block and the partial object + * of updated attributes values. + */ export function getLastBlockAttributesChange( state ) { return state.lastBlockAttributesChange; } diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index c3cad81dfc3b3..91980734acb1e 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -58,6 +58,7 @@ const { getTemplate, getTemplateLock, getBlockListSettings, + getLastBlockAttributesChange, INSERTER_UTILITY_HIGH, INSERTER_UTILITY_MEDIUM, INSERTER_UTILITY_LOW, @@ -2438,4 +2439,14 @@ describe( 'selectors', () => { expect( getBlockListSettings( state, 'chicken' ) ).toBe( undefined ); } ); } ); + + describe( 'getLastBlockAttributesChange', () => { + it( 'returns the last block attributes change', () => { + const state = { lastBlockAttributesChange: [ null, null ] }; + + const result = getLastBlockAttributesChange( state ); + + expect( result ).toEqual( [ null, null ] ); + } ); + } ); } ); From b5b09903c69ecd03c4d18c84348c3718a447652c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 08:24:39 -0400 Subject: [PATCH 14/34] Editor: Fix block sources getDependencies grammar Co-Authored-By: Enrique Piqueras --- packages/editor/src/store/block-sources/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/block-sources/README.md b/packages/editor/src/store/block-sources/README.md index feac1bbde5547..0c16d12b3159d 100644 --- a/packages/editor/src/store/block-sources/README.md +++ b/packages/editor/src/store/block-sources/README.md @@ -11,7 +11,7 @@ These sources are implemented here using a uniform interface for applying and re ### `getDependencies` -Store control called on every store change, expected to return an object whose values represent the data blocks assigned this source depend. When these values change, all blocks assigned this source are automatically updated. The value returned from this function is passed as the second argument of the source's `apply` function, where it is expected to be used as shared data relevant for sourcing the attribute value. +Store control called on every store change, expected to return an object whose values represent the data blocks assigned this source depend on. When these values change, all blocks assigned this source are automatically updated. The value returned from this function is passed as the second argument of the source's `apply` function, where it is expected to be used as shared data relevant for sourcing the attribute value. ### `apply` From 9100f391d04be7522f8bce482bb72d436ad1c998 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 08:25:08 -0400 Subject: [PATCH 15/34] Editor: Fix block sources meta getDependencies JSDoc typo Co-Authored-By: Enrique Piqueras --- packages/editor/src/store/block-sources/meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/block-sources/meta.js b/packages/editor/src/store/block-sources/meta.js index 4ae9434a1d3aa..af203bbbe26e0 100644 --- a/packages/editor/src/store/block-sources/meta.js +++ b/packages/editor/src/store/block-sources/meta.js @@ -9,7 +9,7 @@ import { select } from '@wordpress/data-controls'; import { editPost } from '../actions'; /** - * Store control invoked upon a state changes, responsible for returning an + * Store control invoked upon a state change, responsible for returning an * object of dependencies. When a change in dependencies occurs (by shallow * equality of the returned object), blocks are reset to apply the new sourced * value. From de45deb8b39677422c9b701be458822b4d51efe3 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 08:27:30 -0400 Subject: [PATCH 16/34] Editor: Align block sources getDependencies JSDoc tags Co-Authored-By: Enrique Piqueras --- packages/editor/src/store/block-sources/meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/block-sources/meta.js b/packages/editor/src/store/block-sources/meta.js index af203bbbe26e0..9e6ab3f8cfdd7 100644 --- a/packages/editor/src/store/block-sources/meta.js +++ b/packages/editor/src/store/block-sources/meta.js @@ -14,7 +14,7 @@ import { editPost } from '../actions'; * equality of the returned object), blocks are reset to apply the new sourced * value. * - * @yield {Object} Optional yielded controls. + * @yield {Object} Optional yielded controls. * @return {Object} Dependencies as object. */ export function* getDependencies() { From d318a829e39aff8f0d03bb11aad737ac2f347801 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 08:28:13 -0400 Subject: [PATCH 17/34] Editor: Resolve awaitNextStateChange JSDoc typo Co-Authored-By: Enrique Piqueras --- packages/editor/src/store/controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index a090bc8faef3a..a9698f825fea7 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -4,7 +4,7 @@ import { createRegistryControl } from '@wordpress/data'; /** - * Returns an control descriptor signalling to subscribe to the registry and + * Returns a control descriptor signalling to subscribe to the registry and * resolve the control promise only when the next state change occurs. * * @return {Object} Control descriptor. From eab1ee5ac652e00eced39774b32d5a079428db35 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 08:28:33 -0400 Subject: [PATCH 18/34] Editor: Resolve getRegistry JSDoc typo Co-Authored-By: Enrique Piqueras --- packages/editor/src/store/controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index a9698f825fea7..ff4c82fb77499 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -14,7 +14,7 @@ export function awaitNextStateChange() { } /** - * Returns an control descriptor signalling to resolve with the current data + * Returns a control descriptor signalling to resolve with the current data * registry. * * @return {Object} Control descriptor. From 647263881e0c249db2de4d941153d50bcc84dd77 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 09:15:25 -0400 Subject: [PATCH 19/34] Block Editor: Always unset expected outbound sync value in provider update --- .../block-editor/src/components/provider/index.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index eabd2ac2c1ebc..6cebf4ba6bf21 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -34,12 +34,20 @@ class BlockEditorProvider extends Component { this.attachChangeObserver( registry ); } - if ( this.isSyncingOutcomingValue === value ) { - this.isSyncingOutcomingValue = null; - } else if ( value !== prevProps.value ) { + // Reset a changing value, but only if the new value is not a result of + // an outbound sync initiated by this component. Even when the render + // results from an outbound sync, a reset may still occur if the value + // is modified (not equal by reference), to allow that the consumer may + // apply modifications to reflect back on the editor. + if ( value !== prevProps.value && this.isSyncingOutcomingValue !== value ) { this.isSyncingIncomingValue = value; resetBlocks( value ); } + + // Unset expected outbound value to avoid considering in future render. + if ( this.isSyncingOutcomingValue ) { + this.isSyncingOutcomingValue = null; + } } componentWillUnmount() { From 7263e11f4f1ec91b30d2dbe4047d1d1fe1ce9bda Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 09:48:31 -0400 Subject: [PATCH 20/34] Editor: Detect block attributes change as direct result of last action Infer change as non-null state from lastBlockAttributesChange --- .../src/components/provider/README.md | 7 --- .../src/components/provider/index.js | 14 ----- packages/block-editor/src/store/reducer.js | 18 +++--- packages/block-editor/src/store/selectors.js | 11 ++-- .../block-editor/src/store/test/reducer.js | 36 +++++++---- .../block-editor/src/store/test/selectors.js | 10 ++- .../editor/src/components/provider/index.js | 61 +++---------------- packages/editor/src/store/actions.js | 7 +-- 8 files changed, 56 insertions(+), 108 deletions(-) diff --git a/packages/block-editor/src/components/provider/README.md b/packages/block-editor/src/components/provider/README.md index aac4c3fe9406b..647c5854d0e21 100644 --- a/packages/block-editor/src/components/provider/README.md +++ b/packages/block-editor/src/components/provider/README.md @@ -32,13 +32,6 @@ In the context of an editor, an example usage of this distinction is for managin A callback invoked when the blocks have been modified in a non-persistent manner. Contrasted with `onChange`, a "non-persistent" change is one which is part of a composed input. Any sequence of updates to the same block attribute are treated as non-persistent, except for the first. -### `onBlockAttributesChange` - -* **Type:** `Function` -* **Required** `no` - -A callback to be invoked when a single block's attributes have been updated. The callback is passed the block's `clientId` and an `attributes` object containing only the updated properties of the attributes. The callback is invoked immediately before `onChange` or `onInput`. - ### `children` * **Type:** `WPElement` diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 6cebf4ba6bf21..12fa795ff4865 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -76,24 +76,20 @@ class BlockEditorProvider extends Component { const { getBlocks, isLastBlockChangePersistent, - getLastBlockAttributesChange, __unstableIsLastBlockChangeIgnored, } = registry.select( 'core/block-editor' ); let blocks = getBlocks(); let isPersistent = isLastBlockChangePersistent(); - let lastBlockAttributesChange = getLastBlockAttributesChange(); this.unsubscribe = registry.subscribe( () => { const { onChange, onInput, - onBlockAttributesChange, } = this.props; const newBlocks = getBlocks(); const newIsPersistent = isLastBlockChangePersistent(); - const newLastBlockAttributesChange = getLastBlockAttributesChange(); if ( newBlocks !== blocks && ( @@ -104,19 +100,9 @@ class BlockEditorProvider extends Component { this.isSyncingIncomingValue = null; blocks = newBlocks; isPersistent = newIsPersistent; - lastBlockAttributesChange = newLastBlockAttributesChange; return; } - if ( newLastBlockAttributesChange !== lastBlockAttributesChange ) { - lastBlockAttributesChange = newLastBlockAttributesChange; - - if ( onBlockAttributesChange ) { - const [ clientId, attributes ] = newLastBlockAttributesChange; - onBlockAttributesChange( clientId, attributes ); - } - } - if ( newBlocks !== blocks || // This happens when a previous input is explicitely marked as persistent. diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 64c2d0802db04..8df16d7bda9eb 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1207,28 +1207,30 @@ export const blockListSettings = ( state = {}, action ) => { /** * Reducer return an updated state representing the most recent block attribute - * update. The state is structured as a tuple of the clientId of the block and - * the partial object of updated attributes values. + * update. The state is structured as an object where the keys represent the + * client IDs of blocks, the values a subset of attributes from the most recent + * block update. The state is always reset to null if the last action is + * anything other than an attributes update. * - * @param {[string,Object]} state Current state. - * @param {Object} action Action object. + * @param {Object} state Current state. + * @param {Object} action Action object. * * @return {[string,Object]} Updated state. */ -export function lastBlockAttributesChange( state = [ null, null ], action ) { +export function lastBlockAttributesChange( state, action ) { switch ( action.type ) { case 'UPDATE_BLOCK': if ( ! action.updates.attributes ) { break; } - return [ action.clientId, action.updates.attributes ]; + return { [ action.clientId ]: action.updates.attributes }; case 'UPDATE_BLOCK_ATTRIBUTES': - return [ action.clientId, action.attributes ]; + return { [ action.clientId ]: action.attributes }; } - return state; + return null; } export default combineReducers( { diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 6e890ecbce398..0bf37bcc0fb0a 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1383,16 +1383,13 @@ export function __unstableIsLastBlockChangeIgnored( state ) { } /** - * Returns the last block attributes changed. This is not guaranteed to be a - * result of the most recent action dispatched, so the onus is on the consumer - * to subscribe and compare to own local reference if only interested in the - * most recent action. + * Returns the block attributes changed as a result of the last dispatched + * action. * * @param {Object} state Block editor state. * - * @return {[string,Object]} Most recent block attributes changed, a tuple of - * the clientId of the block and the partial object - * of updated attributes values. + * @return {Object} Subsets of block attributes changed, keyed + * by block client ID. */ export function getLastBlockAttributesChange( state ) { return state.lastBlockAttributesChange; diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index c143a5f8bbfa1..0626f57fb4129 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -2393,14 +2393,14 @@ describe( 'state', () => { } ); describe( 'lastBlockAttributesChange', () => { - it( 'is a tuple, defaulting to null values', () => { + it( 'defaults to null', () => { const state = lastBlockAttributesChange( undefined, {} ); - expect( state ).toEqual( [ null, null ] ); + expect( state ).toBe( null ); } ); it( 'does not return updated value when block update does not include attributes', () => { - const original = deepFreeze( [ null, null ] ); + const original = null; const state = lastBlockAttributesChange( original, { type: 'UPDATE_BLOCK', @@ -2412,7 +2412,7 @@ describe( 'state', () => { } ); it( 'returns updated value when block update includes attributes', () => { - const original = deepFreeze( [ null, null ] ); + const original = null; const state = lastBlockAttributesChange( original, { type: 'UPDATE_BLOCK', @@ -2424,14 +2424,13 @@ describe( 'state', () => { }, } ); - expect( state ).toEqual( [ - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - { food: 'banana' }, - ] ); + expect( state ).toEqual( { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { food: 'banana' }, + } ); } ); it( 'returns updated value when explicit block attributes update', () => { - const original = deepFreeze( [ null, null ] ); + const original = null; const state = lastBlockAttributesChange( original, { type: 'UPDATE_BLOCK_ATTRIBUTES', @@ -2441,10 +2440,21 @@ describe( 'state', () => { }, } ); - expect( state ).toEqual( [ - 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1', - { food: 'banana' }, - ] ); + expect( state ).toEqual( { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { food: 'banana' }, + } ); + } ); + + it( 'returns null on anything other than block attributes update', () => { + const original = deepFreeze( { + 'afd1cb17-2c08-4e7a-91be-007ba7ddc3a1': { food: 'banana' }, + } ); + + const state = lastBlockAttributesChange( original, { + type: '__INERT__', + } ); + + expect( state ).toBe( null ); } ); } ); } ); diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 91980734acb1e..59fff1e28642a 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -2442,11 +2442,17 @@ describe( 'selectors', () => { describe( 'getLastBlockAttributesChange', () => { it( 'returns the last block attributes change', () => { - const state = { lastBlockAttributesChange: [ null, null ] }; + const state = { + lastBlockAttributesChange: { + block1: { fruit: 'bananas' }, + }, + }; const result = getLastBlockAttributesChange( state ); - expect( result ).toEqual( [ null, null ] ); + expect( result ).toEqual( { + block1: { fruit: 'bananas' }, + } ); } ); } ); } ); diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index a4c444d9ba58f..a08629e012f97 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -44,9 +44,6 @@ class EditorProvider extends Component { constructor( props ) { super( ...arguments ); - this.setLastBlockAttributesChange = this.setLastBlockAttributesChange.bind( this ); - this.resetEditorBlocks = this.resetEditorBlocks.bind( this ); - this.resetEditorBlocksWithoutUndoLevel = this.resetEditorBlocksWithoutUndoLevel.bind( this ); this.getBlockEditorSettings = memize( this.getBlockEditorSettings, { maxSize: 1, } ); @@ -138,60 +135,16 @@ class EditorProvider extends Component { this.props.tearDownEditor(); } - /** - * Sets an instance property ahead of a subsequent onChange or onInput - * callback to merge into options an inference that the change resulted - * from the block change. - * - * @param {string} clientId Updated block client ID. - * @param {Object} attributes Subset of updated attributes. - */ - setLastBlockAttributesChange( clientId, attributes ) { - this.lastBlockAttributesChange = { - ...this.lastBlockAttributesChange, - [ clientId ]: attributes, - }; - } - - /** - * Handles block resetting with optional options. - * - * @param {Array} blocks New blocks value. - * @param {?Object} options Optional options. - */ - resetEditorBlocks( blocks, options ) { - if ( this.lastBlockAttributesChange ) { - options = { - ...options, - __unstableLastBlockAttributesChange: this.lastBlockAttributesChange, - }; - - delete this.lastBlockAttributesChange; - } - - this.props.resetEditorBlocks( blocks, options ); - } - - /** - * Handles block resetting with optional options, marking update as to - * avoid creation of an undo level. - * - * @param {Array} blocks New blocks value. - */ - resetEditorBlocksWithoutUndoLevel( blocks ) { - this.resetEditorBlocks( blocks, { - __unstableShouldCreateUndoLevel: true, - } ); - } - render() { const { canUserUseUnfilteredHTML, children, blocks, + resetEditorBlocks, isReady, settings, reusableBlocks, + resetEditorBlocksWithoutUndoLevel, hasUploadPermissions, } = this.props; @@ -209,9 +162,8 @@ class EditorProvider extends Component { return ( @@ -258,6 +210,11 @@ export default compose( [ createWarningNotice, resetEditorBlocks, updateEditorSettings, + resetEditorBlocksWithoutUndoLevel( blocks ) { + resetEditorBlocks( blocks, { + __unstableShouldCreateUndoLevel: false, + } ); + }, tearDownEditor, }; } ), diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index b986617280449..ceb4fefa82584 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -905,10 +905,7 @@ export function unlockPostSaving( lockName ) { * @return {Object} Action object */ export function* resetEditorBlocks( blocks, options = {} ) { - const { - __unstableLastBlockAttributesChange: lastBlockAttributesChange, - __unstableShouldCreateUndoLevel: shouldCreateUndoLevel = true, - } = options; + const lastBlockAttributesChange = yield select( 'core/block-editor', 'getLastBlockAttributesChange' ); // Sync to sources from block attributes updates. if ( lastBlockAttributesChange ) { @@ -948,7 +945,7 @@ export function* resetEditorBlocks( blocks, options = {} ) { return { type: 'RESET_EDITOR_BLOCKS', blocks: yield* getBlocksWithSourcedAttributes( blocks ), - shouldCreateUndoLevel, + shouldCreateUndoLevel: options.__unstableShouldCreateUndoLevel !== false, }; } From d643af0db6448d167ee26b892b7f82920999f266 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 09:52:41 -0400 Subject: [PATCH 21/34] Documentation: Regenerate editor, block-editor data documentation --- .../developers/data/data-core-block-editor.md | 13 +++++++++++++ .../developers/data/data-core-editor.md | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md index 1b42d76df3e3b..39fb0f6fa1f11 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -335,6 +335,19 @@ _Returns_ - `Array`: Items that appear in inserter. +# **getLastBlockAttributesChange** + +Returns the block attributes changed as a result of the last dispatched +action. + +_Parameters_ + +- _state_ `Object`: Block editor state. + +_Returns_ + +- `Object`: Subsets of block attributes changed, keyed by block client ID. + # **getLastMultiSelectedBlockClientId** Returns the client ID of the last block in the multi-selection set, or null diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index e3456d65168db..971619b48e478 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -1302,12 +1302,26 @@ _Related_ - stopTyping in core/block-editor store. +# **subscribeSources** + +Returns an action generator which loops to await the next state change, +calling to reset blocks when a block source dependencies change. + # **synchronizeTemplate** _Related_ - synchronizeTemplate in core/block-editor store. +# **tearDownEditor** + +Returns an action object signalling that the editor is being destroyed and +that any necessary state or side-effect cleanup should occur. + +_Returns_ + +- `Object`: Action object. + # **toggleBlockMode** _Related_ From d9c95e07c39180f3ac321c94f3e1149cc2ada992 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 11:14:54 -0400 Subject: [PATCH 22/34] Block Editor: Unset outbound sync value in editor provider reset condition --- .../src/components/provider/index.js | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 12fa795ff4865..33eff22a08892 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -34,20 +34,23 @@ class BlockEditorProvider extends Component { this.attachChangeObserver( registry ); } - // Reset a changing value, but only if the new value is not a result of - // an outbound sync initiated by this component. Even when the render - // results from an outbound sync, a reset may still occur if the value - // is modified (not equal by reference), to allow that the consumer may - // apply modifications to reflect back on the editor. - if ( value !== prevProps.value && this.isSyncingOutcomingValue !== value ) { + if ( this.isSyncingOutcomingValue !== null && this.isSyncingOutcomingValue === value ) { + // Skip block reset if the value matches expected outbound sync + // triggered by this component by a preceding change detection. + // Only skip if the value matches expectation, since a reset should + // still occur if the value is modified (not equal by reference), + // to allow that the consumer may apply modifications to reflect + // back on the editor. + this.isSyncingOutcomingValue = null; + } else if ( value !== prevProps.value ) { + // Reset changing value in all other cases than the sync described + // above. Since this can be reached in an update following an out- + // bound sync, unset the outbound value to avoid considering it in + // subsequent renders. + this.isSyncingOutcomingValue = null; this.isSyncingIncomingValue = value; resetBlocks( value ); } - - // Unset expected outbound value to avoid considering in future render. - if ( this.isSyncingOutcomingValue ) { - this.isSyncingOutcomingValue = null; - } } componentWillUnmount() { From 54c1f48307f1a763558ffaf59fa7032675fd04df Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 11:16:23 -0400 Subject: [PATCH 23/34] Editor: Update getDependencies JSDoc to separate yield grouping --- packages/editor/src/store/block-sources/meta.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/store/block-sources/meta.js b/packages/editor/src/store/block-sources/meta.js index 9e6ab3f8cfdd7..3910395c4a740 100644 --- a/packages/editor/src/store/block-sources/meta.js +++ b/packages/editor/src/store/block-sources/meta.js @@ -14,7 +14,8 @@ import { editPost } from '../actions'; * equality of the returned object), blocks are reset to apply the new sourced * value. * - * @yield {Object} Optional yielded controls. + * @yield {Object} Optional yielded controls. + * * @return {Object} Dependencies as object. */ export function* getDependencies() { From 22476001d7ed300730d636267c9c2e9b3be4cf8d Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 12:26:06 -0400 Subject: [PATCH 24/34] Editor: Dispatch SETUP_EDITOR prior to blocks reset SETUP_EDITOR is responsible for setting the initial edits, which are expected to be unset by a subsequent block reset --- packages/editor/src/store/actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index ceb4fefa82584..08e86907142a7 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -177,14 +177,14 @@ export function* setupEditor( post, edits, template ) { yield resetPost( post ); yield* resetLastBlockSourceDependencies(); - yield resetEditorBlocks( blocks ); - yield setupEditorState( post ); yield { type: 'SETUP_EDITOR', post, edits, template, }; + yield resetEditorBlocks( blocks ); + yield setupEditorState( post ); yield* subscribeSources(); } From 814e04ad8377efedee27fb3a9fae6b2f33ba71cb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 12:27:41 -0400 Subject: [PATCH 25/34] Editor: Update actions tests per reordered SETUP_EDITOR --- packages/editor/src/store/test/actions.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index 437bfc051d119..ff10dcf0a9044 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -658,6 +658,13 @@ describe( 'Editor actions', () => { const { value } = fulfillment.next(); expect( value ).toEqual( actions.resetPost( post ) ); } ); + it( 'should yield the SETUP_EDITOR action', () => { + const { value } = fulfillment.next(); + expect( value ).toEqual( { + type: 'SETUP_EDITOR', + post: { content: { raw: '' }, status: 'publish' }, + } ); + } ); it( 'should yield action object for resetEditorBlocks', () => { const { value } = fulfillment.next(); expect( value ).toEqual( actions.resetEditorBlocks( [] ) ); @@ -670,13 +677,6 @@ describe( 'Editor actions', () => { ) ); } ); - it( 'should yield the SETUP_EDITOR action', () => { - const { value } = fulfillment.next(); - expect( value ).toEqual( { - type: 'SETUP_EDITOR', - post: { content: { raw: '' }, status: 'publish' }, - } ); - } ); } ); describe( 'resetPost', () => { From c237ccbf61c59dab06f50ef703ae967658cbbc00 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 15:28:05 -0400 Subject: [PATCH 26/34] Block API: Stub default attributes, keywords values for block type registration --- packages/blocks/CHANGELOG.md | 4 + packages/blocks/src/api/registration.js | 80 +++++++++++++++----- packages/blocks/src/api/test/registration.js | 46 +++++++++++ packages/blocks/src/api/utils.js | 12 +-- 4 files changed, 115 insertions(+), 27 deletions(-) diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index d38762e51694a..fdbc957dd802e 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -1,5 +1,9 @@ ## Master +### Improvements + +- Omitting `attributes` or `keywords` settings will now stub default values (an empty object or empty array, respectively). + ### Bug Fixes - The `'blocks.registerBlockType'` filter is now applied to each of a block's deprecated settings as well as the block's main settings. Ensures `supports` settings like `anchor` work for deprecations. diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 83dcc057c94e6..498f83627e6ff 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -21,30 +21,72 @@ import { select, dispatch } from '@wordpress/data'; */ import { isValidIcon, normalizeIconObject } from './utils'; +/** + * Render behavior of a block type icon; one of a Dashicon slug, an element, + * or a component. + * + * @typedef {(string|WPElement|WPComponent)} WPBlockTypeIconRender + * + * @see https://developer.wordpress.org/resource/dashicons/ + */ + +/** + * An object describing a normalized block type icon. + * + * @typedef {WPBlockTypeIconDescriptor} + * + * @property {WPBlockTypeIconRender} src Render behavior of the icon, + * one of a Dashicon slug, an + * element, or a component. + * @property {string} background Optimal background hex string + * color when displaying icon. + * @property {string} foreground Optimal foreground hex string + * color when displaying icon. + * @property {string} shadowColor Optimal shadow hex string + * color when displaying icon. + */ + +/** + * Value to use to render the icon for a block type in an editor interface, + * either a Dashicon slug, an element, a component, or an object describing + * the icon. + * + * @typedef {(WPBlockTypeIconDescriptor|WPBlockTypeIconRender)} WPBlockTypeIcon + */ + /** * Defined behavior of a block type. * * @typedef {WPBlockType} * - * @property {string} name Block's namespaced name. - * @property {string} title Human-readable label for a block. - * Shown in the block inserter. - * @property {string} category Category classification of block, - * impacting where block is shown in - * inserter results. - * @property {(Object|string|WPElement)} icon Slug of the Dashicon to be shown - * as the icon for the block in the - * inserter, or element or an object describing the icon. - * @property {?string[]} keywords Additional keywords to produce - * block as inserter search result. - * @property {?Object} attributes Block attributes. - * @property {?Function} save Serialize behavior of a block, - * returning an element describing - * structure of the block's post - * content markup. - * @property {WPComponent} edit Component rendering element to be - * interacted with in an editor. + * @property {string} name Block type's namespaced name. + * @property {string} title Human-readable block type label. + * @property {string} category Block type category classification, + * used in search interfaces to arrange + * block types by category. + * @property {?WPBlockTypeIcon} icon Block type icon. + * @property {?string[]} keywords Additional keywords to produce block + * type as result in search interfaces. + * @property {?Object} attributes Block type attributes. + * @property {?WPComponent} save Optional component describing + * serialized markup structure of a + * block type. + * @property {WPComponent} edit Component rendering an element to + * manipulate the attributes of a block + * in the context of an editor. + */ + +/** + * Default values to assign for omitted optional block type settings. + * + * @type {Object} */ +const DEFAULT_BLOCK_TYPE_SETTINGS = { + icon: 'block-default', + attributes: {}, + keywords: [], + save: () => null, +}; let serverSideBlockDefinitions = {}; @@ -74,7 +116,7 @@ export function unstable__bootstrapServerSideBlockDefinitions( definitions ) { / export function registerBlockType( name, settings ) { settings = { name, - save: () => null, + ...DEFAULT_BLOCK_TYPE_SETTINGS, ...get( serverSideBlockDefinitions, name ), ...settings, }; diff --git a/packages/blocks/src/api/test/registration.js b/packages/blocks/src/api/test/registration.js index cd8c9692c21a5..30df36c9359d4 100644 --- a/packages/blocks/src/api/test/registration.js +++ b/packages/blocks/src/api/test/registration.js @@ -95,6 +95,8 @@ describe( 'blocks', () => { icon: { src: 'block-default', }, + attributes: {}, + keywords: [], save: noop, category: 'common', title: 'block title', @@ -111,6 +113,8 @@ describe( 'blocks', () => { it( 'should reject blocks with invalid save function', () => { const block = registerBlockType( 'my-plugin/fancy-block-5', { ...defaultBlockSettings, + attributes: {}, + keywords: [], save: 'invalid', } ); expect( console ).toHaveErroredWith( 'The "save" property must be a valid function.' ); @@ -159,6 +163,25 @@ describe( 'blocks', () => { expect( block ).toBeUndefined(); } ); + it( 'should assign default settings', () => { + registerBlockType( 'core/test-block-with-defaults', { + title: 'block title', + category: 'common', + } ); + + expect( getBlockType( 'core/test-block-with-defaults' ) ).toEqual( { + name: 'core/test-block-with-defaults', + title: 'block title', + category: 'common', + icon: { + src: 'block-default', + }, + attributes: {}, + keywords: [], + save: expect.any( Function ), + } ); + } ); + it( 'should default to browser-initialized global attributes', () => { const attributes = { ok: { type: 'boolean' } }; unstable__bootstrapServerSideBlockDefinitions( { @@ -181,6 +204,7 @@ describe( 'blocks', () => { type: 'boolean', }, }, + keywords: [], } ); } ); @@ -218,6 +242,8 @@ describe( 'blocks', () => { fill="red" stroke="blue" strokeWidth="10" /> ), }, + attributes: {}, + keywords: [], } ); } ); @@ -237,6 +263,8 @@ describe( 'blocks', () => { icon: { src: 'foo', }, + attributes: {}, + keywords: [], } ); } ); @@ -262,6 +290,8 @@ describe( 'blocks', () => { icon: { src: MyTestIcon, }, + attributes: {}, + keywords: [], } ); } ); @@ -293,6 +323,8 @@ describe( 'blocks', () => { fill="red" stroke="blue" strokeWidth="10" /> ), }, + attributes: {}, + keywords: [], } ); } ); @@ -309,6 +341,8 @@ describe( 'blocks', () => { icon: { src: 'block-default', }, + attributes: {}, + keywords: [], } ); } ); @@ -394,6 +428,8 @@ describe( 'blocks', () => { icon: { src: 'block-default', }, + attributes: {}, + keywords: [], }, ] ); const oldBlock = unregisterBlockType( 'core/test-block' ); @@ -406,6 +442,8 @@ describe( 'blocks', () => { icon: { src: 'block-default', }, + attributes: {}, + keywords: [], } ); expect( getBlockTypes() ).toEqual( [] ); } ); @@ -478,6 +516,8 @@ describe( 'blocks', () => { icon: { src: 'block-default', }, + attributes: {}, + keywords: [], } ); } ); @@ -493,6 +533,8 @@ describe( 'blocks', () => { icon: { src: 'block-default', }, + attributes: {}, + keywords: [], } ); } ); } ); @@ -515,6 +557,8 @@ describe( 'blocks', () => { icon: { src: 'block-default', }, + attributes: {}, + keywords: [], }, { name: 'core/test-block-with-settings', @@ -525,6 +569,8 @@ describe( 'blocks', () => { icon: { src: 'block-default', }, + attributes: {}, + keywords: [], }, ] ); } ); diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 6dcabbcbcea01..99dc98b5c3204 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -77,17 +77,13 @@ export function isValidIcon( icon ) { * and returns a new icon object that is normalized so we can rely on just on possible icon structure * in the codebase. * - * @param {(Object|string|WPElement)} icon Slug of the Dashicon to be shown - * as the icon for the block in the - * inserter, or element or an object describing the icon. + * @param {WPBlockTypeIconRender} icon Render behavior of a block type icon; + * one of a Dashicon slug, an element, or a + * component. * - * @return {Object} Object describing the icon. + * @return {WPBlockTypeIconDescriptor} Object describing the icon. */ export function normalizeIconObject( icon ) { - if ( ! icon ) { - icon = 'block-default'; - } - if ( isValidIcon( icon ) ) { return { src: icon }; } From 3e097ef65e3ca13733c320f7817bdf7c5a435e3e Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 16:03:27 -0400 Subject: [PATCH 27/34] Block API: Update documentation per formation of WPBlockTypeIconDescriptor typedef --- packages/blocks/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/blocks/README.md b/packages/blocks/README.md index e456eb546ffbb..331be71c3a15f 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -548,11 +548,11 @@ in the codebase. _Parameters_ -- _icon_ `(Object|string|WPElement)`: Slug of the Dashicon to be shown as the icon for the block in the inserter, or element or an object describing the icon. +- _icon_ `WPBlockTypeIconRender`: Render behavior of a block type icon; one of a Dashicon slug, an element, or a component. _Returns_ -- `Object`: Object describing the icon. +- `WPBlockTypeIconDescriptor`: Object describing the icon. # **parse** From c12bee46bdb3f6b1baf17ed8b7b18b4e1d2c3dea Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 16:03:50 -0400 Subject: [PATCH 28/34] Editor: Iterate last block changes in updating sources --- packages/editor/src/store/actions.js | 33 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 08e86907142a7..990f35f0ddf0e 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -910,28 +910,27 @@ export function* resetEditorBlocks( blocks, options = {} ) { // Sync to sources from block attributes updates. if ( lastBlockAttributesChange ) { const updatedSources = new Set; - - const blockStack = [ ...blocks ]; - - for ( let i = 0; i < blockStack.length; i++ ) { - const block = blockStack[ i ]; - - // Account for changes occurring within nested block. - if ( block.innerBlocks.length ) { - blockStack.push( ...block.innerBlocks ); - } - - if ( ! lastBlockAttributesChange.hasOwnProperty( block.clientId ) ) { + const updatedBlockTypes = new Set; + for ( const [ clientId, attributes ] of Object.entries( lastBlockAttributesChange ) ) { + const blockName = yield select( 'core/block-editor', 'getBlockName', clientId ); + if ( updatedBlockTypes.has( blockName ) ) { continue; } - const blockType = yield select( 'core/blocks', 'getBlockType', block.name ); - const changedAttributes = lastBlockAttributesChange[ block.clientId ]; + updatedBlockTypes.add( blockName ); + const blockType = yield select( 'core/blocks', 'getBlockType', blockName ); for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { - if ( changedAttributes.hasOwnProperty( attributeName ) && sources[ schema.source ] && sources[ schema.source ].update ) { - yield* sources[ schema.source ].update( schema, changedAttributes[ attributeName ] ); - updatedSources.add( sources[ schema.source ] ); + const source = sources[ schema.source ]; + + if ( + attributes.hasOwnProperty( attributeName ) && + source && + source.update && + ! updatedSources.has( source ) + ) { + yield* source.update( schema, attributes[ attributeName ] ); + updatedSources.add( source ); } } } From ac2acaf179538ab87ec618691645d8604992a4e3 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 9 Jul 2019 16:24:57 -0400 Subject: [PATCH 29/34] Editor: Update source even if already updated for block updates A block may have multiple attributes which independently source uniquely from the same source type (e.g. two different meta properties) --- packages/editor/src/store/actions.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 990f35f0ddf0e..6c4533b1edc38 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -923,12 +923,7 @@ export function* resetEditorBlocks( blocks, options = {} ) { for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { const source = sources[ schema.source ]; - if ( - attributes.hasOwnProperty( attributeName ) && - source && - source.update && - ! updatedSources.has( source ) - ) { + if ( attributes.hasOwnProperty( attributeName ) && source && source.update ) { yield* source.update( schema, attributes[ attributeName ] ); updatedSources.add( source ); } From 075ffabea194c88c99d59115919b98c922618053 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 10 Jul 2019 11:28:06 -0400 Subject: [PATCH 30/34] Editor: Iterate updated attributes from block reset source update Optimization since the attributes are likely a subset (most likely a single attribute), so we can avoid iterating all attributes defined in a block type. --- packages/editor/src/store/actions.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 6c4533b1edc38..4bbc688464394 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -920,11 +920,16 @@ export function* resetEditorBlocks( blocks, options = {} ) { updatedBlockTypes.add( blockName ); const blockType = yield select( 'core/blocks', 'getBlockType', blockName ); - for ( const [ attributeName, schema ] of Object.entries( blockType.attributes ) ) { + for ( const [ attributeName, newAttributeValue ] of Object.entries( attributes ) ) { + if ( ! blockType.attributes.hasOwnProperty( attributeName ) ) { + continue; + } + + const schema = blockType.attributes[ attributeName ]; const source = sources[ schema.source ]; if ( attributes.hasOwnProperty( attributeName ) && source && source.update ) { - yield* source.update( schema, attributes[ attributeName ] ); + yield* source.update( schema, newAttributeValue ); updatedSources.add( source ); } } From 6f6625729909835812bb2a2f38edc959b13da228 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 10 Jul 2019 11:53:12 -0400 Subject: [PATCH 31/34] Editor: Mark custom sources selectors, actions as experimental --- .../developers/data/data-core-block-editor.md | 13 ------------- .../developers/data/data-core-editor.md | 14 -------------- packages/block-editor/src/store/selectors.js | 2 +- packages/block-editor/src/store/test/selectors.js | 6 +++--- packages/editor/src/components/provider/index.js | 4 ++-- packages/editor/src/store/actions.js | 8 ++++---- 6 files changed, 10 insertions(+), 37 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md index 39fb0f6fa1f11..1b42d76df3e3b 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -335,19 +335,6 @@ _Returns_ - `Array`: Items that appear in inserter. -# **getLastBlockAttributesChange** - -Returns the block attributes changed as a result of the last dispatched -action. - -_Parameters_ - -- _state_ `Object`: Block editor state. - -_Returns_ - -- `Object`: Subsets of block attributes changed, keyed by block client ID. - # **getLastMultiSelectedBlockClientId** Returns the client ID of the last block in the multi-selection set, or null diff --git a/docs/designers-developers/developers/data/data-core-editor.md b/docs/designers-developers/developers/data/data-core-editor.md index 971619b48e478..e3456d65168db 100644 --- a/docs/designers-developers/developers/data/data-core-editor.md +++ b/docs/designers-developers/developers/data/data-core-editor.md @@ -1302,26 +1302,12 @@ _Related_ - stopTyping in core/block-editor store. -# **subscribeSources** - -Returns an action generator which loops to await the next state change, -calling to reset blocks when a block source dependencies change. - # **synchronizeTemplate** _Related_ - synchronizeTemplate in core/block-editor store. -# **tearDownEditor** - -Returns an action object signalling that the editor is being destroyed and -that any necessary state or side-effect cleanup should occur. - -_Returns_ - -- `Object`: Action object. - # **toggleBlockMode** _Related_ diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 0bf37bcc0fb0a..7f6a22b6494dc 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1391,7 +1391,7 @@ export function __unstableIsLastBlockChangeIgnored( state ) { * @return {Object} Subsets of block attributes changed, keyed * by block client ID. */ -export function getLastBlockAttributesChange( state ) { +export function __experimentalGetLastBlockAttributesChange( state ) { return state.lastBlockAttributesChange; } diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 59fff1e28642a..1b6db908dc0be 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -58,7 +58,7 @@ const { getTemplate, getTemplateLock, getBlockListSettings, - getLastBlockAttributesChange, + __experimentalGetLastBlockAttributesChange, INSERTER_UTILITY_HIGH, INSERTER_UTILITY_MEDIUM, INSERTER_UTILITY_LOW, @@ -2440,7 +2440,7 @@ describe( 'selectors', () => { } ); } ); - describe( 'getLastBlockAttributesChange', () => { + describe( '__experimentalGetLastBlockAttributesChange', () => { it( 'returns the last block attributes change', () => { const state = { lastBlockAttributesChange: { @@ -2448,7 +2448,7 @@ describe( 'selectors', () => { }, }; - const result = getLastBlockAttributesChange( state ); + const result = __experimentalGetLastBlockAttributesChange( state ); expect( result ).toEqual( { block1: { fruit: 'bananas' }, diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index a08629e012f97..cfa67ddbb615d 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -200,7 +200,7 @@ export default compose( [ updatePostLock, resetEditorBlocks, updateEditorSettings, - tearDownEditor, + __experimentalTearDownEditor, } = dispatch( 'core/editor' ); const { createWarningNotice } = dispatch( 'core/notices' ); @@ -215,7 +215,7 @@ export default compose( [ __unstableShouldCreateUndoLevel: false, } ); }, - tearDownEditor, + tearDownEditor: __experimentalTearDownEditor, }; } ), ] )( EditorProvider ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 4bbc688464394..4d9f748115015 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -185,7 +185,7 @@ export function* setupEditor( post, edits, template ) { }; yield resetEditorBlocks( blocks ); yield setupEditorState( post ); - yield* subscribeSources(); + yield* __experimentalSubscribeSources(); } /** @@ -194,7 +194,7 @@ export function* setupEditor( post, edits, template ) { * * @return {Object} Action object. */ -export function tearDownEditor() { +export function __experimentalTearDownEditor() { return { type: 'TEAR_DOWN_EDITOR' }; } @@ -204,7 +204,7 @@ export function tearDownEditor() { * * @yield {Object} Action object. */ -export function* subscribeSources() { +export function* __experimentalSubscribeSources() { while ( true ) { const registry = yield awaitNextStateChange(); @@ -905,7 +905,7 @@ export function unlockPostSaving( lockName ) { * @return {Object} Action object */ export function* resetEditorBlocks( blocks, options = {} ) { - const lastBlockAttributesChange = yield select( 'core/block-editor', 'getLastBlockAttributesChange' ); + const lastBlockAttributesChange = yield select( 'core/block-editor', '__experimentalGetLastBlockAttributesChange' ); // Sync to sources from block attributes updates. if ( lastBlockAttributesChange ) { From 8516fd1d45d216623f1f09f2aca17e70bcfd9144 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 10 Jul 2019 12:10:44 -0400 Subject: [PATCH 32/34] Editor: Remove redundant Object#hasOwnProperty in iterating attributes --- packages/editor/src/store/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 4d9f748115015..e6ffbff4d5483 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -928,7 +928,7 @@ export function* resetEditorBlocks( blocks, options = {} ) { const schema = blockType.attributes[ attributeName ]; const source = sources[ schema.source ]; - if ( attributes.hasOwnProperty( attributeName ) && source && source.update ) { + if ( source && source.update ) { yield* source.update( schema, newAttributeValue ); updatedSources.add( source ); } From 37a7ba93b736dec35a5f4bfd1cf72d0ca58fbd82 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 10 Jul 2019 12:12:19 -0400 Subject: [PATCH 33/34] Block Editor: Rename getLastBlockAttributesChange to getLastBlockAttributeChanges --- packages/block-editor/src/store/selectors.js | 2 +- packages/block-editor/src/store/test/selectors.js | 6 +++--- packages/editor/src/store/actions.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 7f6a22b6494dc..5e8771ff94935 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1391,7 +1391,7 @@ export function __unstableIsLastBlockChangeIgnored( state ) { * @return {Object} Subsets of block attributes changed, keyed * by block client ID. */ -export function __experimentalGetLastBlockAttributesChange( state ) { +export function __experimentalGetLastBlockAttributeChanges( state ) { return state.lastBlockAttributesChange; } diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 1b6db908dc0be..4e1289e44aefc 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -58,7 +58,7 @@ const { getTemplate, getTemplateLock, getBlockListSettings, - __experimentalGetLastBlockAttributesChange, + __experimentalGetLastBlockAttributeChanges, INSERTER_UTILITY_HIGH, INSERTER_UTILITY_MEDIUM, INSERTER_UTILITY_LOW, @@ -2440,7 +2440,7 @@ describe( 'selectors', () => { } ); } ); - describe( '__experimentalGetLastBlockAttributesChange', () => { + describe( '__experimentalGetLastBlockAttributeChanges', () => { it( 'returns the last block attributes change', () => { const state = { lastBlockAttributesChange: { @@ -2448,7 +2448,7 @@ describe( 'selectors', () => { }, }; - const result = __experimentalGetLastBlockAttributesChange( state ); + const result = __experimentalGetLastBlockAttributeChanges( state ); expect( result ).toEqual( { block1: { fruit: 'bananas' }, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index e6ffbff4d5483..ae31857cad45c 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -905,7 +905,7 @@ export function unlockPostSaving( lockName ) { * @return {Object} Action object */ export function* resetEditorBlocks( blocks, options = {} ) { - const lastBlockAttributesChange = yield select( 'core/block-editor', '__experimentalGetLastBlockAttributesChange' ); + const lastBlockAttributesChange = yield select( 'core/block-editor', '__experimentalGetLastBlockAttributeChanges' ); // Sync to sources from block attributes updates. if ( lastBlockAttributesChange ) { From 468f9088c8d5611737baca7f55bce8fb48b6d75f Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Wed, 10 Jul 2019 14:14:41 -0400 Subject: [PATCH 34/34] Editor: Use getRegistry control for next change subscription --- packages/editor/src/store/actions.js | 4 +++- packages/editor/src/store/controls.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index ae31857cad45c..8ea2c9a408de8 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -206,7 +206,7 @@ export function __experimentalTearDownEditor() { */ export function* __experimentalSubscribeSources() { while ( true ) { - const registry = yield awaitNextStateChange(); + yield awaitNextStateChange(); // The bailout case: If the editor becomes unmounted, it will flag // itself as non-ready. Effectively unsubscribes from the registry. @@ -215,6 +215,8 @@ export function* __experimentalSubscribeSources() { break; } + const registry = yield getRegistry(); + let reset = false; for ( const source of Object.values( sources ) ) { if ( ! source.getDependencies ) { diff --git a/packages/editor/src/store/controls.js b/packages/editor/src/store/controls.js index ff4c82fb77499..3b0288d00b0a5 100644 --- a/packages/editor/src/store/controls.js +++ b/packages/editor/src/store/controls.js @@ -28,7 +28,7 @@ const controls = { ( registry ) => () => new Promise( ( resolve ) => { const unsubscribe = registry.subscribe( () => { unsubscribe(); - resolve( registry ); + resolve(); } ); } ) ),