diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index aac68e2e6faa88..f1b86d615d99fa 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'; /** * WordPress dependencies @@ -649,42 +649,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/index.js b/packages/block-editor/src/components/provider/index.js index 6d06246ca9170a..e6c381f898f8eb 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 ); } } @@ -87,7 +87,7 @@ class BlockEditorProvider extends Component { __unstableIsLastBlockChangeIgnored() ) ) { - this.isSyncingIncomingValue = false; + this.isSyncingIncomingValue = null; blocks = newBlocks; isPersistent = newIsPersistent; return; @@ -101,7 +101,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; diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 6633611fbd9b19..866a6f11369ea5 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 new reference when the inner blocks of a given block client ID * change. This is used exclusively as a memoized selector dependant, relying @@ -130,42 +122,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, @@ -192,7 +156,8 @@ export const getBlock = createSelector( }; }, ( state, clientId ) => [ - ...getBlockAttributes.getDependants( state, clientId ), + state.blocks.byClientId[ clientId ], + state.blocks.attributes[ clientId ], getBlockDependantsCacheBust( state, clientId ), ] ); @@ -211,7 +176,7 @@ export const __unstableGetBlockWithoutInnerBlocks = createSelector( }, ( state, clientId ) => [ state.blocks.byClientId[ clientId ], - ...getBlockAttributes.getDependants( state, clientId ), + state.blocks.attributes[ clientId ], ] ); @@ -314,7 +279,6 @@ export const getBlocksByClientId = createSelector( ( clientId ) => getBlock( state, clientId ) ), ( state ) => [ - getPostMeta( state ), state.blocks.byClientId, state.blocks.order, state.blocks.attributes, @@ -691,7 +655,6 @@ export const getMultiSelectedBlocks = createSelector( state.blocks.byClientId, state.blocks.order, state.blocks.attributes, - getPostMeta( state ), ] ); @@ -1455,22 +1418,6 @@ 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 ] ); -} - /** * Returns the available reusable blocks * diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index c038a40621eff6..f2e465b00f181b 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -473,54 +473,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: [], - }, - }, - }; - - 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/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index b40532957c5f05..dd0335d11a8d2c 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -454,7 +454,7 @@ export function createBlockWithFallback( blockNode ) { // provided source value with the serialized output before there are any modifications to // the block. When both match, the block is marked as valid. if ( ! isFallbackBlock ) { - block.isValid = isValidBlockContent( blockType, block.attributes, innerHTML ); + // block.isValid = isValidBlockContent( blockType, block.attributes, innerHTML ); } // Preserve original content for future use in case the block is parsed as diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index cc9533a0a79a7b..bfdc1fe1e338ae 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -9,7 +9,7 @@ import memize from 'memize'; */ import { compose } from '@wordpress/compose'; import { Component } from '@wordpress/element'; -import { withDispatch, withSelect } from '@wordpress/data'; +import { withDispatch, withSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { BlockEditorProvider, transformStyles } from '@wordpress/block-editor'; import apiFetch from '@wordpress/api-fetch'; @@ -20,6 +20,7 @@ import { decodeEntities } from '@wordpress/html-entities'; * Internal dependencies */ import withRegistryProvider from './with-registry-provider'; +import createUseBlockSources from './use-block-sources'; import { mediaUpload } from '../../utils'; import ReusableBlocksButtons from '../reusable-blocks-buttons'; import ConvertToGroupButtons from '../convert-to-group-buttons'; @@ -40,6 +41,22 @@ const fetchLinkSuggestions = async ( search ) => { } ) ); }; +const useBlockSources = createUseBlockSources( [ + () => { + const { editPost } = useDispatch( 'core/editor' ); + + return [ 'meta', { + onChange: ( key, value ) => editPost( { meta: { [ key ]: value } } ), + } ]; + }, +] ); + +function BlockEditorProviderWithSources( props ) { + const blocks = useBlockSources( props.value ); + + return ; +} + class EditorProvider extends Component { constructor( props ) { super( ...arguments ); @@ -72,7 +89,7 @@ class EditorProvider extends Component { } } - getBlockEditorSettings( settings, meta, onMetaChange, reusableBlocks, hasUploadPermissions ) { + getBlockEditorSettings( settings, reusableBlocks, hasUploadPermissions ) { return { ...pick( settings, [ 'alignWide', @@ -95,10 +112,6 @@ class EditorProvider extends Component { 'templateLock', 'titlePlaceholder', ] ), - __experimentalMetaSource: { - value: meta, - onChange: onMetaChange, - }, __experimentalReusableBlocks: reusableBlocks, __experimentalMediaUpload: hasUploadPermissions ? mediaUpload : undefined, __experimentalFetchLinkSuggestions: fetchLinkSuggestions, @@ -136,8 +149,6 @@ class EditorProvider extends Component { resetEditorBlocks, isReady, settings, - meta, - onMetaChange, reusableBlocks, resetEditorBlocksWithoutUndoLevel, hasUploadPermissions, @@ -148,11 +159,11 @@ class EditorProvider extends Component { } const editorSettings = this.getBlockEditorSettings( - settings, meta, onMetaChange, reusableBlocks, hasUploadPermissions + settings, reusableBlocks, hasUploadPermissions ); return ( - - + ); } } @@ -173,7 +184,6 @@ export default compose( [ const { __unstableIsEditorReady: isEditorReady, getEditorBlocks, - getEditedPostAttribute, __experimentalGetReusableBlocks, } = select( 'core/editor' ); const { canUser } = select( 'core' ); @@ -181,7 +191,6 @@ export default compose( [ return { isReady: isEditorReady(), blocks: getEditorBlocks(), - meta: getEditedPostAttribute( 'meta' ), reusableBlocks: __experimentalGetReusableBlocks(), hasUploadPermissions: defaultTo( canUser( 'create', 'media' ), true ), }; @@ -191,7 +200,6 @@ export default compose( [ setupEditor, updatePostLock, resetEditorBlocks, - editPost, updateEditorSettings, } = dispatch( 'core/editor' ); const { createWarningNotice } = dispatch( 'core/notices' ); @@ -207,9 +215,6 @@ export default compose( [ __unstableShouldCreateUndoLevel: false, } ); }, - onMetaChange( meta ) { - editPost( { meta } ); - }, }; } ), ] )( EditorProvider ); diff --git a/packages/editor/src/components/provider/use-block-sources.js b/packages/editor/src/components/provider/use-block-sources.js new file mode 100644 index 00000000000000..40edccdd2e9feb --- /dev/null +++ b/packages/editor/src/components/provider/use-block-sources.js @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +import { reduce, find, pick, keyBy, has, forEach } from 'lodash'; +import memoize from 'memize'; + +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { useRef, useEffect, useMemo } from '@wordpress/element'; + +const SOURCE_SCHEMA_VALUE_KEY_MAPPING = { + meta: 'meta', + post: 'property', +}; + +const createUseBlockSources = ( handlerCreators ) => ( blocks ) => { + const handlers = handlerCreators.reduce( ( result, runHook ) => { + const [ source, handler ] = runHook(); + result[ source ] = handler; + return result; + }, {} ); + + const { + sources, + blockTypes, + } = useSelect( ( select ) => { + const { getSources } = select( 'core/editor' ); + const { getBlockTypes } = select( 'core/blocks' ); + + return { + sources: getSources(), + blockTypes: getBlockTypes(), + }; + } ); + + const blockTypesByName = useMemo( + () => keyBy( blockTypes, 'name' ), + [ blockTypes ] + ); + + const getBlockTypeAttributesBySource = useMemo( + () => memoize( ( blockType, source ) => ( + reduce( blockType.attributes, ( result, schema, name ) => ( + schema.source === source ? [ ...result, name ] : result + ), [] ) + ) ), + [ blockTypesByName ] + ); + + const { setSourceValues, resetBlocks } = useDispatch( 'core/editor' ); + + const previousBlocks = useRef(); + + useEffect( () => { + const { current: previousBlocksValue } = previousBlocks; + const isSyncing = ! previousBlocksValue; + previousBlocks.current = blocks; + + if ( isSyncing ) { + return; + } + + blocks.forEach( ( block ) => { + for ( const sourceName in sources ) { + const attributeNames = getBlockTypeAttributesBySource( blockTypesByName[ block.name ], sourceName ); + if ( ! attributeNames.length ) { + continue; + } + + const { clientId } = block; + const previousBlock = find( previousBlocksValue, { clientId } ); + if ( ! previousBlock || block !== previousBlock ) { + const nextSourceValues = pick( block.attributes, attributeNames ); + if ( has( handlers, [ sourceName, 'onChange' ] ) ) { + forEach( nextSourceValues, ( value, key ) => handlers[ sourceName ].onChange( key, value ) ); + } + setSourceValues( sourceName, nextSourceValues ); + } + } + } ); + }, [ blocks ] ); + + useEffect( () => { + const nextBlocks = blocks.map( ( block ) => { + let workingBlock = block; + + const blockType = blockTypesByName[ block.name ]; + for ( const [ sourceName, sourceValues ] of Object.entries( sources ) ) { + const attributeNames = getBlockTypeAttributesBySource( blockType, sourceName ); + if ( ! attributeNames.length ) { + continue; + } + + if ( workingBlock === block ) { + workingBlock = { ...workingBlock }; + } + + for ( const attributeName of attributeNames ) { + const key = blockType.attributes[ attributeName ][ SOURCE_SCHEMA_VALUE_KEY_MAPPING[ sourceName ] ]; + workingBlock.attributes[ attributeName ] = sourceValues[ key ]; + } + } + + return block; + } ); + + if ( nextBlocks !== blocks ) { + // Reset previous blocks to treat as sync, to avoid recursion. + previousBlocks.current = null; + resetBlocks( nextBlocks ); + } + }, [ sources ] ); + + return blocks; +}; + +export default createUseBlockSources; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 073887663bd1b9..39d49d55e9ff4f 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -59,7 +59,7 @@ export function* setupEditor( post, edits, template ) { content = post.content.raw; } - let blocks = parse( content ); + let blocks = parse( content, { validate: false } ); // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; @@ -67,6 +67,7 @@ export function* setupEditor( post, edits, template ) { blocks = synchronizeBlocksWithTemplate( blocks, template ); } + yield resetSourceValues( 'meta', post.meta ); yield resetEditorBlocks( blocks ); yield setupEditorState( post ); } @@ -79,7 +80,9 @@ export function* setupEditor( post, edits, template ) { * * @return {Object} Action object. */ -export function resetPost( post ) { +export function* resetPost( post ) { + yield resetSourceValues( 'meta', post.meta ); + return { type: 'RESET_POST', post, @@ -228,7 +231,11 @@ export function setupEditorState( post ) { * * @return {Object} Action object. */ -export function editPost( edits ) { +export function* editPost( edits ) { + if ( 'meta' in edits ) { + yield setSourceValues( 'meta', edits.meta ); + } + return { type: 'EDIT_POST', edits, @@ -752,6 +759,22 @@ export function updateEditorSettings( settings ) { }; } +export function resetSourceValues( source, values ) { + return { + type: 'RESET_SOURCE_VALUES', + source, + values, + }; +} + +export function setSourceValues( source, values ) { + return { + type: 'SET_SOURCE_VALUES', + source, + values, + }; +} + /** * Backward compatibility */ diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index a6f2bd4a4565ac..ded131c0598b89 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -583,6 +583,27 @@ export function editorSettings( state = EDITOR_SETTINGS_DEFAULTS, action ) { return state; } +export function sources( state = {}, action ) { + switch ( action.type ) { + case 'RESET_SOURCE_VALUES': + return { + ...state, + [ action.source ]: action.values, + }; + + case 'SET_SOURCE_VALUES': + return { + ...state, + [ action.source ]: { + ...state[ action.source ], + ...action.values, + }, + }; + } + + return state; +} + export default optimist( combineReducers( { editor, initialEdits, @@ -596,4 +617,5 @@ export default optimist( combineReducers( { postSavingLock, isReady, editorSettings, + sources, } ) ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 7570e8b7848de6..cfb3c2f7374fc3 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1154,6 +1154,10 @@ export function getEditorSettings( state ) { return state.editorSettings; } +export function getSources( state ) { + return state.sources; +} + /* * Backward compatibility */