diff --git a/backport-changelog/6.9/9992.md b/backport-changelog/6.9/9992.md new file mode 100644 index 00000000000000..9ca717940358c0 --- /dev/null +++ b/backport-changelog/6.9/9992.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/9992 + +* https://github.com/WordPress/gutenberg/pull/71820 \ No newline at end of file diff --git a/lib/compat/wordpress-6.9/block-bindings.php b/lib/compat/wordpress-6.9/block-bindings.php index f46a2dce398ce6..ff78a2cac40bc8 100644 --- a/lib/compat/wordpress-6.9/block-bindings.php +++ b/lib/compat/wordpress-6.9/block-bindings.php @@ -24,6 +24,21 @@ function ( $attributes, $block_type ) { 2 ); +// The following filter can be removed once the minimum required WordPress version is 6.9 or newer. +add_filter( + 'block_editor_settings_all', + function ( $editor_settings ) { + $editor_settings['__experimentalBlockBindingsSupportedAttributes'] = array(); + foreach ( array_keys( WP_Block_Type_Registry::get_instance()->get_all_registered() ) as $block_type ) { + $supported_block_attributes = gutenberg_get_block_bindings_supported_attributes( $block_type ); + if ( ! empty( $supported_block_attributes ) ) { + $editor_settings['__experimentalBlockBindingsSupportedAttributes'][ $block_type ] = $supported_block_attributes; + } + } + return $editor_settings; + } +); + /** * Callback function for the render_block filter. * @@ -86,6 +101,59 @@ function gutenberg_block_bindings_render_block( $block_content, $block, $instanc } add_filter( 'render_block', 'gutenberg_block_bindings_render_block', 10, 3 ); +/** + * Retrieves the list of block attributes supported by block bindings. + * + * @since 6.9.0 + * + * @param string $block_type The block type whose supported attributes are being retrieved. + * @return array The list of block attributes that are supported by block bindings. + */ +function gutenberg_get_block_bindings_supported_attributes( $block_type ) { + // List of block attributes supported by Block Bindings in WP 6.8. + $block_bindings_supported_attributes_6_8 = array( + 'core/paragraph' => array( 'content' ), + 'core/heading' => array( 'content' ), + 'core/image' => array( 'id', 'url', 'title', 'alt' ), + 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), + ); + + $supported_block_attributes = + $block_bindings_supported_attributes_6_8[ $block_type ] ?? + array(); + + /** + * Filters the supported block attributes for block bindings. + * + * @since 6.9.0 + * + * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings. + * @param string $block_type The block type whose attributes are being filtered. + */ + $supported_block_attributes = apply_filters( + 'block_bindings_supported_attributes', + $supported_block_attributes, + $block_type + ); + + /** + * Filters the supported block attributes for block bindings. + * + * The dynamic portion of the hook name, `$block_type`, refers to the block type + * whose attributes are being filtered. + * + * @since 6.9.0 + * + * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings. + */ + $supported_block_attributes = apply_filters( + "block_bindings_supported_attributes_{$block_type}", + $supported_block_attributes + ); + + return $supported_block_attributes; +} + /** * Processes the block bindings and updates the block attributes with the values from the sources. * @@ -138,38 +206,8 @@ function gutenberg_process_block_bindings( $instance ) { 'core/image' => array( 'id', 'url', 'title', 'alt' ), 'core/button' => array( 'url', 'text', 'linkTarget', 'rel' ), ); - $supported_block_attributes = - $block_bindings_supported_attributes_6_8[ $block_type ] ?? - array(); - - /** - * Filters the supported block attributes for block bindings. - * - * @since 6.9.0 - * - * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings. - * @param string $block_type The block type whose attributes are being filtered. - */ - $supported_block_attributes = apply_filters( - 'block_bindings_supported_attributes', - $supported_block_attributes, - $block_type - ); - /** - * Filters the supported block attributes for block bindings. - * - * The dynamic portion of the hook name, `$block_type`, refers to the block type - * whose attributes are being filtered. - * - * @since 6.9.0 - * - * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings. - */ - $supported_block_attributes = apply_filters( - "block_bindings_supported_attributes_{$block_type}", - $supported_block_attributes - ); + $supported_block_attributes = gutenberg_get_block_bindings_supported_attributes( $block_type ); /* * Remove attributes that we know are processed by WP 6.8 from the list, diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 27d3650f3a0902..3d368786a595fa 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -22,10 +22,10 @@ import { useCallback, useContext, useMemo } from '@wordpress/element'; import BlockContext from '../block-context'; import isURLLike from '../link-control/is-url-like'; import { - canBindAttribute, hasPatternOverridesDefaultBinding, replacePatternOverridesDefaultBinding, } from '../../utils/block-bindings'; +import { store as blockEditorStore } from '../../store'; import { unlock } from '../../lock-unlock'; /** @@ -56,6 +56,8 @@ const Edit = ( props ) => { const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); +const EMPTY_ARRAY = []; + const EditWithGeneratedProps = ( props ) => { const { name, clientId, attributes, setAttributes } = props; const registry = useRegistry(); @@ -66,6 +68,17 @@ const EditWithGeneratedProps = ( props ) => { unlock( select( blocksStore ) ).getAllBlockBindingsSources(), [] ); + const bindableAttributes = useSelect( + ( select ) => { + const { __experimentalBlockBindingsSupportedAttributes } = + select( blockEditorStore ).getSettings(); + return ( + __experimentalBlockBindingsSupportedAttributes?.[ name ] || + EMPTY_ARRAY + ); + }, + [ name ] + ); const { blockBindings, context, hasPatternOverrides } = useMemo( () => { // Assign context values using the block type's declared context needs. @@ -90,8 +103,8 @@ const EditWithGeneratedProps = ( props ) => { } return { blockBindings: replacePatternOverridesDefaultBinding( - name, - attributes?.metadata?.bindings + attributes?.metadata?.bindings, + bindableAttributes ), context: computedContext, hasPatternOverrides: hasPatternOverridesDefaultBinding( @@ -120,7 +133,10 @@ const EditWithGeneratedProps = ( props ) => { ) ) { const { source: sourceName, args: sourceArgs } = binding; const source = registeredSources[ sourceName ]; - if ( ! source || ! canBindAttribute( name, attributeName ) ) { + if ( + ! source || + ! bindableAttributes.includes( attributeName ) + ) { continue; } @@ -172,6 +188,7 @@ const EditWithGeneratedProps = ( props ) => { }, [ attributes, + bindableAttributes, blockBindings, clientId, context, @@ -197,7 +214,7 @@ const EditWithGeneratedProps = ( props ) => { ) ) { if ( ! blockBindings[ attributeName ] || - ! canBindAttribute( name, attributeName ) + ! bindableAttributes.includes( attributeName ) ) { continue; } @@ -250,6 +267,7 @@ const EditWithGeneratedProps = ( props ) => { } ); }, [ + bindableAttributes, blockBindings, clientId, context, diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 8d9e952c5929b5..d9eb1658e5c455 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -29,7 +29,6 @@ import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; import { useScrollIntoView } from './use-scroll-into-view'; import { useFlashEditableBlocks } from '../../use-flash-editable-blocks'; -import { canBindBlock } from '../../../utils/block-bindings'; import { useFirefoxDraggableCompatibility } from './use-firefox-draggable-compatibility'; /** @@ -128,14 +127,13 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { const blockEditContext = useBlockEditContext(); const hasBlockBindings = !! blockEditContext[ blockBindingsKey ]; - const bindingsStyle = - hasBlockBindings && canBindBlock( name ) - ? { - '--wp-admin-theme-color': 'var(--wp-block-synced-color)', - '--wp-admin-theme-color--rgb': - 'var(--wp-block-synced-color--rgb)', - } - : {}; + const bindingsStyle = hasBlockBindings + ? { + '--wp-admin-theme-color': 'var(--wp-block-synced-color)', + '--wp-admin-theme-color--rgb': + 'var(--wp-block-synced-color--rgb)', + } + : {}; // Ensures it warns only inside the `edit` implementation for the block. if ( blockApiVersion < 2 && clientId === blockEditContext.clientId ) { diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index ac33f7f98d2968..062e5749756d9a 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -39,7 +39,6 @@ import FormatEdit from './format-edit'; import { getAllowedFormats } from './utils'; import { Content, valueToHTMLString } from './content'; import { withDeprecations } from './with-deprecations'; -import { canBindBlock } from '../../utils/block-bindings'; import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); @@ -177,9 +176,14 @@ export function RichTextWrapper( const { disableBoundBlock, bindingsPlaceholder, bindingsLabel } = useSelect( ( select ) => { + const { __experimentalBlockBindingsSupportedAttributes } = + select( blockEditorStore ).getSettings(); + if ( ! blockBindings?.[ identifier ] || - ! canBindBlock( blockName ) + ! ( + blockName in __experimentalBlockBindingsSupportedAttributes + ) ) { return {}; } diff --git a/packages/block-editor/src/hooks/block-bindings.js b/packages/block-editor/src/hooks/block-bindings.js index 11e17aba3b30da..2911aac1b256c1 100644 --- a/packages/block-editor/src/hooks/block-bindings.js +++ b/packages/block-editor/src/hooks/block-bindings.js @@ -23,11 +23,7 @@ import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies */ -import { - canBindAttribute, - getBindableAttributes, - useBlockBindingsUtils, -} from '../utils/block-bindings'; +import { useBlockBindingsUtils } from '../utils/block-bindings'; import { unlock } from '../lock-unlock'; import InspectorControls from '../components/inspector-controls'; import BlockContext from '../components/block-context'; @@ -205,52 +201,62 @@ function EditableBlockBindingsPanelItems( { export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const blockContext = useContext( BlockContext ); const { removeAllBlockBindings } = useBlockBindingsUtils(); - const bindableAttributes = getBindableAttributes( blockName ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); // `useSelect` is used purposely here to ensure `getFieldsList` // is updated whenever there are updates in block context. // `source.getFieldsList` may also call a selector via `select`. const _fieldsList = {}; - const { fieldsList, canUpdateBlockBindings } = useSelect( - ( select ) => { - if ( ! bindableAttributes || bindableAttributes.length === 0 ) { - return EMPTY_OBJECT; - } - const registeredSources = getBlockBindingsSources(); - Object.entries( registeredSources ).forEach( - ( [ sourceName, { getFieldsList, usesContext } ] ) => { - if ( getFieldsList ) { - // Populate context. - const context = {}; - if ( usesContext?.length ) { - for ( const key of usesContext ) { - context[ key ] = blockContext[ key ]; + const { bindableAttributes, fieldsList, canUpdateBlockBindings } = + useSelect( + ( select ) => { + const { __experimentalBlockBindingsSupportedAttributes } = + select( blockEditorStore ).getSettings(); + const _bindableAttributes = + __experimentalBlockBindingsSupportedAttributes?.[ + blockName + ]; + if ( + ! _bindableAttributes || + _bindableAttributes.length === 0 + ) { + return EMPTY_OBJECT; + } + const registeredSources = getBlockBindingsSources(); + Object.entries( registeredSources ).forEach( + ( [ sourceName, { getFieldsList, usesContext } ] ) => { + if ( getFieldsList ) { + // Populate context. + const context = {}; + if ( usesContext?.length ) { + for ( const key of usesContext ) { + context[ key ] = blockContext[ key ]; + } + } + const sourceList = getFieldsList( { + select, + context, + } ); + // Only add source if the list is not empty. + if ( Object.keys( sourceList || {} ).length ) { + _fieldsList[ sourceName ] = { ...sourceList }; } - } - const sourceList = getFieldsList( { - select, - context, - } ); - // Only add source if the list is not empty. - if ( Object.keys( sourceList || {} ).length ) { - _fieldsList[ sourceName ] = { ...sourceList }; } } - } - ); - return { - fieldsList: - Object.values( _fieldsList ).length > 0 - ? _fieldsList - : EMPTY_OBJECT, - canUpdateBlockBindings: - select( blockEditorStore ).getSettings() - .canUpdateBlockBindings, - }; - }, - [ blockContext, bindableAttributes ] - ); + ); + return { + bindableAttributes: _bindableAttributes, + fieldsList: + Object.values( _fieldsList ).length > 0 + ? _fieldsList + : EMPTY_OBJECT, + canUpdateBlockBindings: + select( blockEditorStore ).getSettings() + .canUpdateBlockBindings, + }; + }, + [ blockContext ] + ); // Return early if there are no bindable attributes. if ( ! bindableAttributes || bindableAttributes.length === 0 ) { return null; @@ -260,7 +266,7 @@ export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const filteredBindings = { ...bindings }; Object.keys( filteredBindings ).forEach( ( key ) => { if ( - ! canBindAttribute( blockName, key ) || + ! bindableAttributes.includes( key ) && filteredBindings[ key ].source === 'core/pattern-overrides' ) { delete filteredBindings[ key ]; diff --git a/packages/block-editor/src/utils/block-bindings.js b/packages/block-editor/src/utils/block-bindings.js index f7f44e2f599430..57c58560021293 100644 --- a/packages/block-editor/src/utils/block-bindings.js +++ b/packages/block-editor/src/utils/block-bindings.js @@ -11,13 +11,6 @@ import { useBlockEditContext } from '../components/block-edit'; const DEFAULT_ATTRIBUTE = '__default'; const PATTERN_OVERRIDES_SOURCE = 'core/pattern-overrides'; -const BLOCK_BINDINGS_ALLOWED_BLOCKS = { - 'core/paragraph': [ 'content' ], - 'core/heading': [ 'content' ], - 'core/image': [ 'id', 'url', 'title', 'alt', 'caption' ], - 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ], - 'core/post-date': [ 'datetime' ], -}; /** * Checks if the given object is empty. @@ -30,43 +23,6 @@ function isObjectEmpty( object ) { return ! object || Object.keys( object ).length === 0; } -/** - * Based on the given block name, checks if it is possible to bind the block. - * - * @param {string} blockName The name of the block. - * - * @return {boolean} Whether it is possible to bind the block to sources. - */ -export function canBindBlock( blockName ) { - return blockName in BLOCK_BINDINGS_ALLOWED_BLOCKS; -} - -/** - * Based on the given block name and attribute name, checks if it is possible to bind the block attribute. - * - * @param {string} blockName The name of the block. - * @param {string} attributeName The name of attribute. - * - * @return {boolean} Whether it is possible to bind the block attribute. - */ -export function canBindAttribute( blockName, attributeName ) { - return ( - canBindBlock( blockName ) && - BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ].includes( attributeName ) - ); -} - -/** - * Gets the bindable attributes for a given block. - * - * @param {string} blockName The name of the block. - * - * @return {string[]} The bindable attributes for the block. - */ -export function getBindableAttributes( blockName ) { - return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; -} - /** * Checks if the block has the `__default` binding for pattern overrides. * @@ -85,15 +41,17 @@ export function hasPatternOverridesDefaultBinding( bindings ) { * - bindings passed in: `{ __default: { source: 'core/pattern-overrides' } }` * - bindings returned: `{ content: { source: 'core/pattern-overrides' } }` * - * @param {string} blockName The block name (e.g. 'core/paragraph'). - * @param {?Record} bindings A block's bindings from the metadata attribute. + * @param {?Record} bindings A block's bindings from the metadata attribute. + * @param {string[]} supportedAttributes The block's attributes which are supported by block bindings. * * @return {Object} The bindings with default replaced for pattern overrides. */ -export function replacePatternOverridesDefaultBinding( blockName, bindings ) { +export function replacePatternOverridesDefaultBinding( + bindings, + supportedAttributes +) { // The `__default` binding currently only works for pattern overrides. if ( hasPatternOverridesDefaultBinding( bindings ) ) { - const supportedAttributes = BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ]; const bindingsWithDefaults = {}; for ( const attributeName of supportedAttributes ) { // If the block has mixed binding sources, retain any non pattern override bindings. diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 6d1fbfde945176..136bb88dd10fae 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -40,6 +40,7 @@ function __experimentalReusableBlocksSelect( select ) { } const BLOCK_EDITOR_SETTINGS = [ + '__experimentalBlockBindingsSupportedAttributes', '__experimentalBlockDirectory', '__experimentalDiscussionSettings', '__experimentalFeatures',