-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Block Bindings: Disable editing of bound block attributes in editor UI #58085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
2f071ad
57ab9b2
445405f
af7f870
c3567a4
35c8c64
989f456
a4dc34a
3515538
41709fa
1567f17
1984749
9644946
478f861
2acf6bd
a8a6da3
30b635e
a6f5fde
7d0cb9a
e6a5a4d
7c1ca5a
4246260
5a0cee8
54c313d
aaf8652
1f34e08
895e6dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -376,7 +376,7 @@ export function RichTextWrapper( | |
| useFirefoxCompat(), | ||
| anchorRef, | ||
| ] ) } | ||
| contentEditable={ true } | ||
| contentEditable={ props.isContentBound ? false : true } | ||
|
||
| suppressContentEditableWarning={ true } | ||
| className={ classnames( | ||
| 'block-editor-rich-text__editable', | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,144 @@ | ||||
| /** | ||||
| * WordPress dependencies | ||||
| */ | ||||
| import { createHigherOrderComponent } from '@wordpress/compose'; | ||||
| import { useRegistry, useSelect } from '@wordpress/data'; | ||||
| import { addFilter } from '@wordpress/hooks'; | ||||
| /** | ||||
| * Internal dependencies | ||||
| */ | ||||
| import { store as blockEditorStore } from '../store'; | ||||
| import { useBlockEditContext } from '../components/block-edit/context'; | ||||
| import { unlock } from '../lock-unlock'; | ||||
|
|
||||
| /** @typedef {import('@wordpress/compose').WPHigherOrderComponent} WPHigherOrderComponent */ | ||||
| /** @typedef {import('@wordpress/blocks').WPBlockSettings} WPBlockSettings */ | ||||
|
|
||||
| /** | ||||
| * Given a binding of block attributes, returns a higher order component that | ||||
| * overrides its `attributes` and `setAttributes` props to sync any changes needed. | ||||
| * | ||||
| * @return {WPHigherOrderComponent} Higher-order component. | ||||
| */ | ||||
|
|
||||
| const BLOCK_BINDINGS_ALLOWED_BLOCKS = { | ||||
| 'core/paragraph': [ 'content' ], | ||||
| 'core/heading': [ 'content' ], | ||||
| 'core/image': [ 'url', 'title', 'alt' ], | ||||
| 'core/button': [ 'url', 'text' ], | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also add the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There were some issues with |
||||
| }; | ||||
|
|
||||
| const createEditFunctionWithBindingsAttribute = () => | ||||
| createHigherOrderComponent( | ||||
| ( BlockEdit ) => ( props ) => { | ||||
| const { clientId } = useBlockEditContext(); | ||||
|
|
||||
| const { | ||||
| getBlockBindingsSource, | ||||
| getBlockAttributes, | ||||
| updateBlockAttributes, | ||||
| } = useSelect( ( select ) => { | ||||
| return { | ||||
| getBlockBindingsSource: unlock( select( blockEditorStore ) ) | ||||
| .getBlockBindingsSource, | ||||
| getBlockAttributes: | ||||
| select( blockEditorStore ).getBlockAttributes, | ||||
| updateBlockAttributes: | ||||
| select( blockEditorStore ).updateBlockAttributes, | ||||
| }; | ||||
| }, [] ); | ||||
|
|
||||
| const updatedAttributes = getBlockAttributes( clientId ); | ||||
| if ( updatedAttributes?.metadata?.bindings ) { | ||||
| Object.entries( updatedAttributes.metadata.bindings ).forEach( | ||||
| ( [ attributeName, settings ] ) => { | ||||
| const source = getBlockBindingsSource( | ||||
| settings.source.name | ||||
| ); | ||||
|
|
||||
| if ( source ) { | ||||
| // Second argument (`updateMetaValue`) will be used to update the value in the future. | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any idea or advance of how it's going to implement the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The technical implementation is almost there. However, there are many things to take into account, and it was decided not to rush things for the upcoming 6.5 to ensure that whatever we land is stable enough. |
||||
| const { | ||||
| placeholder, | ||||
| useValue: [ metaValue = null ] = [], | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good feedback. On the server, there is |
||||
| } = source.useSource( | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a hook? You cannot use hooks inside conditions and loops
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the case of post meta source, it uses If that's a problem, do you have any ideas on how that should be handled? We need to iterate through the different bindings and get the value from each of the sources.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The only solution is to probably mount sub components, get the result, and set some state in this component, similar to how it is done here:
But I'd love to hear other people's thoughts
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch @ellatrix! Yup, we'll have to change it because it is a hook. |
||||
| props, | ||||
| settings.source.attributes | ||||
| ); | ||||
|
|
||||
| if ( placeholder ) { | ||||
| updatedAttributes.placeholder = placeholder; | ||||
| updatedAttributes[ attributeName ] = null; | ||||
| } | ||||
|
|
||||
| if ( metaValue ) { | ||||
michalczaplinski marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| updatedAttributes[ attributeName ] = metaValue; | ||||
| } | ||||
| } | ||||
| } | ||||
| ); | ||||
| } | ||||
|
|
||||
| const registry = useRegistry(); | ||||
|
|
||||
| return ( | ||||
| <> | ||||
| <BlockEdit | ||||
| key="edit" | ||||
| attributes={ updatedAttributes } | ||||
| setAttributes={ ( newAttributes, blockId ) => | ||||
| registry.batch( () => | ||||
| updateBlockAttributes( blockId, newAttributes ) | ||||
| ) | ||||
| } | ||||
|
Comment on lines
+93
to
+97
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
P.S. There is no need to wrap the returned
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Mamaduka, I think the issue at the moment is that users will only change the attribute when it is not bound. When the value is sourced from the block binding then the attribute becomes readonly. In effect, the batching isn't really necessary and even there should be no override set until it's possible to edit the external source in UI. By the way, feel free to refactor the code here if you have some ideas how to optimize it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is that handled somewhere else? Because I don't see that logic here. If the block calls the Sorry, I'm not super familiar with the internals of Block Binding API. I just came across this code while looking for something else.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There is no way to trigger the update from UI at the moment for supported blocks and their selected attributes. Direct changes to the store don't matter because the value will get replaced on the frontend anyway. You are totally right that batching is not needed at the moment and most likely the override for
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for confirming. I can push refactoring PR later today. |
||||
| { ...props } | ||||
| /> | ||||
| </> | ||||
| ); | ||||
| }, | ||||
| 'useBoundAttributes' | ||||
| ); | ||||
|
|
||||
| /** | ||||
| * Filters a registered block's settings to enhance a block's `edit` component | ||||
| * to upgrade bound attributes. | ||||
| * | ||||
| * @param {WPBlockSettings} settings Registered block settings. | ||||
| * | ||||
| * @return {WPBlockSettings} Filtered block settings. | ||||
| */ | ||||
| function shimAttributeSource( settings ) { | ||||
| if ( ! ( settings.name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { | ||||
| return settings; | ||||
| } | ||||
| settings.edit = createEditFunctionWithBindingsAttribute()( settings.edit ); | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What stops us from using the new "api" here? This will cause a bit of a performance regression.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to modify the |
||||
|
|
||||
| return settings; | ||||
| } | ||||
|
|
||||
| addFilter( | ||||
| 'blocks.registerBlockType', | ||||
| 'core/editor/custom-sources-backwards-compatibility/shim-attribute-source', | ||||
| shimAttributeSource | ||||
| ); | ||||
|
|
||||
| // Add the context to all blocks. | ||||
| addFilter( | ||||
| 'blocks.registerBlockType', | ||||
| 'core/block-bindings-ui', | ||||
| ( settings, name ) => { | ||||
| if ( ! ( name in BLOCK_BINDINGS_ALLOWED_BLOCKS ) ) { | ||||
| return settings; | ||||
| } | ||||
| const contextItems = [ 'postId', 'postType', 'queryId' ]; | ||||
| const usesContextArray = settings.usesContext; | ||||
| const oldUsesContextArray = new Set( usesContextArray ); | ||||
| contextItems.forEach( ( item ) => { | ||||
| if ( ! oldUsesContextArray.has( item ) ) { | ||||
| usesContextArray.push( item ); | ||||
| } | ||||
| } ); | ||||
| settings.usesContext = usesContextArray; | ||||
| return settings; | ||||
| } | ||||
| ); | ||||
SantosGuillamot marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -164,6 +164,7 @@ function ButtonEdit( props ) { | |
| text, | ||
| url, | ||
| width, | ||
| metadata, | ||
| } = attributes; | ||
|
|
||
| const TagName = tagName || 'a'; | ||
|
|
@@ -276,6 +277,7 @@ function ButtonEdit( props ) { | |
| onReplace={ onReplace } | ||
| onMerge={ mergeBlocks } | ||
| identifier="text" | ||
| isContentBound={ metadata?.bindings?.text } | ||
| /> | ||
| </div> | ||
| <BlockControls group="block"> | ||
|
|
@@ -287,7 +289,7 @@ function ButtonEdit( props ) { | |
| } } | ||
| /> | ||
| ) } | ||
| { ! isURLSet && isLinkTag && ( | ||
| { ! isURLSet && isLinkTag && ! metadata?.bindings?.url && ( | ||
| <ToolbarButton | ||
| name="link" | ||
| icon={ link } | ||
|
|
@@ -296,7 +298,7 @@ function ButtonEdit( props ) { | |
| onClick={ startEditing } | ||
| /> | ||
| ) } | ||
| { isURLSet && isLinkTag && ( | ||
| { isURLSet && isLinkTag && ! metadata?.bindings?.url && ( | ||
|
||
| <ToolbarButton | ||
| name="link" | ||
| icon={ linkOff } | ||
|
|
@@ -307,43 +309,46 @@ function ButtonEdit( props ) { | |
| /> | ||
| ) } | ||
| </BlockControls> | ||
| { isLinkTag && isSelected && ( isEditingURL || isURLSet ) && ( | ||
| <Popover | ||
| placement="bottom" | ||
| onClose={ () => { | ||
| setIsEditingURL( false ); | ||
| richTextRef.current?.focus(); | ||
| } } | ||
| anchor={ popoverAnchor } | ||
| focusOnMount={ isEditingURL ? 'firstElement' : false } | ||
| __unstableSlotName={ '__unstable-block-tools-after' } | ||
| shift | ||
| > | ||
| <LinkControl | ||
| value={ linkValue } | ||
| onChange={ ( { | ||
| url: newURL, | ||
| opensInNewTab: newOpensInNewTab, | ||
| nofollow: newNofollow, | ||
| } ) => | ||
| setAttributes( | ||
| getUpdatedLinkAttributes( { | ||
| rel, | ||
| url: newURL, | ||
| opensInNewTab: newOpensInNewTab, | ||
| nofollow: newNofollow, | ||
| } ) | ||
| ) | ||
| } | ||
| onRemove={ () => { | ||
| unlink(); | ||
| { isLinkTag && | ||
| isSelected && | ||
| ( isEditingURL || isURLSet ) && | ||
| ! metadata?.bindings?.url && ( | ||
| <Popover | ||
| placement="bottom" | ||
| onClose={ () => { | ||
| setIsEditingURL( false ); | ||
| richTextRef.current?.focus(); | ||
| } } | ||
| forceIsEditingLink={ isEditingURL } | ||
| settings={ LINK_SETTINGS } | ||
| /> | ||
| </Popover> | ||
| ) } | ||
| anchor={ popoverAnchor } | ||
| focusOnMount={ isEditingURL ? 'firstElement' : false } | ||
| __unstableSlotName={ '__unstable-block-tools-after' } | ||
| shift | ||
| > | ||
| <LinkControl | ||
| value={ linkValue } | ||
| onChange={ ( { | ||
| url: newURL, | ||
| opensInNewTab: newOpensInNewTab, | ||
| nofollow: newNofollow, | ||
| } ) => | ||
| setAttributes( | ||
| getUpdatedLinkAttributes( { | ||
| rel, | ||
| url: newURL, | ||
| opensInNewTab: newOpensInNewTab, | ||
| nofollow: newNofollow, | ||
| } ) | ||
| ) | ||
| } | ||
| onRemove={ () => { | ||
| unlink(); | ||
| richTextRef.current?.focus(); | ||
| } } | ||
| forceIsEditingLink={ isEditingURL } | ||
| settings={ LINK_SETTINGS } | ||
| /> | ||
| </Popover> | ||
| ) } | ||
| <InspectorControls> | ||
| <WidthPanel | ||
| selectedWidth={ width } | ||
|
|
||

Uh oh!
There was an error while loading. Please reload this page.