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
*/