Skip to content
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

Update the Site Title block to use block bindings #67260

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ Displays the name of this site. Update the block, and the changes apply everywhe
- **Name:** core/site-title
- **Category:** theme
- **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** isLink, level, levelOptions, linkTarget, textAlign
- **Attributes:** content, isLink, level, levelOptions, linkTarget, textAlign

## Social Icon

Expand Down
55 changes: 55 additions & 0 deletions lib/compat/wordpress-6.8/block-bindings/site.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
/**
* Suite source for the block bindings.
*
* @since 6.8.0
* @package WordPress
* @subpackage Block Bindings
*/


if ( ! function_exists( '_block_bindings_site_get_value' ) ) {
/**
* Gets value for Site source.
*
* @since 6.8.0
* @access private
*
* @param array $source_args Array containing source arguments used to look up the override value.
* Example: array( "key" => "foo" ).
* @param WP_Block $block_instance The block instance.
* @return mixed The value computed for the source.
*/
function _block_bindings_site_get_value( array $source_args ) {
if ( empty( $source_args['key'] ) ) {
return null;
}

if ( 'title' === $source_args['key'] ) {
return esc_html( get_bloginfo( 'name' ) );
}

return null;
}
}


if ( ! function_exists( '_register_block_bindings_site_source' ) ) {
/**
* Registers Site source in the block bindings registry.
*
* @since 6.8.0
* @access private
*/
function _register_block_bindings_site_source() {
register_block_bindings_source(
'core/site',
array(
'label' => _x( 'Site', 'block bindings source' ),
'get_value_callback' => '_block_bindings_site_get_value',
)
);
}

add_action( 'init', '_register_block_bindings_site_source' );
}
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.8/blocks.php';
require __DIR__ . '/compat/wordpress-6.8/functions.php';
require __DIR__ . '/compat/wordpress-6.8/post.php';
require __DIR__ . '/compat/wordpress-6.8/block-bindings/site.php';

// Experimental features.
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
Expand Down
44 changes: 35 additions & 9 deletions packages/block-editor/src/hooks/use-bindings-attributes.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { store as blocksStore } from '@wordpress/blocks';
import { store as blocksStore, getBlockType } from '@wordpress/blocks';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useRegistry, useSelect } from '@wordpress/data';
import { useCallback, useMemo, useContext } from '@wordpress/element';
Expand Down Expand Up @@ -29,6 +29,7 @@ const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
'core/heading': [ 'content' ],
'core/image': [ 'id', 'url', 'title', 'alt' ],
'core/button': [ 'url', 'text', 'linkTarget', 'rel' ],
'core/site-title': [ 'content' ],
};

const DEFAULT_ATTRIBUTE = '__default';
Expand Down Expand Up @@ -66,6 +67,31 @@ function replacePatternOverrideDefaultBindings( blockName, bindings ) {
return bindings;
}

/**
* Adds block type bindings defined in block.json to the existing bindings object.
* Block type bindings are defined in the block's attributes using the `binding` property.
*
* @param {string} blockName The name of the block (e.g. 'core/paragraph')
* @param {Object} bindings The existing bindings object to add to.
*
* @return {Object} The merged bindings object containing both existing and block type bindings
*/
function addBlockTypeBindings( blockName, bindings ) {
const settings = getBlockType( blockName );
const attributes = settings.attributes ?? {};
const blockTypeBindings = {};
Object.keys( attributes ).forEach( ( attributeName ) => {
if ( attributes[ attributeName ].binding ) {
blockTypeBindings[ attributeName ] =
attributes[ attributeName ].binding;
}
} );
return {
...bindings,
...blockTypeBindings,
};
}

/**
* Based on the given block name,
* check if it is possible to bind the block.
Expand Down Expand Up @@ -104,14 +130,14 @@ export const withBlockBindingSupport = createHigherOrderComponent(
unlock( select( blocksStore ) ).getAllBlockBindingsSources()
);
const { name, clientId, context, setAttributes } = props;
const blockBindings = useMemo(
() =>
replacePatternOverrideDefaultBindings(
name,
props.attributes.metadata?.bindings
),
[ props.attributes.metadata?.bindings, name ]
);
const blockBindings = useMemo( () => {
const bindings = replacePatternOverrideDefaultBindings(
name,
props.attributes.metadata?.bindings
);

return addBlockTypeBindings( name, bindings );
}, [ props.attributes.metadata?.bindings, name ] );

// While this hook doesn't directly call any selectors, `useSelect` is
// used purposely here to ensure `boundAttributes` is updated whenever
Expand Down
12 changes: 12 additions & 0 deletions packages/block-library/src/site-title/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
"description": "Displays the name of this site. Update the block, and the changes apply everywhere it’s used. This will also appear in the browser title bar and in search results.",
"textdomain": "default",
"attributes": {
"content": {
"type": "rich-text",
"source": "rich-text",
"selector": "p,h1,h2,h3,h4,h5,h6",
"role": "content",
"binding": {
"source": "core/site",
"args": {
"key": "title"
}
}
},
"level": {
"type": "number",
"default": 1
Expand Down
46 changes: 17 additions & 29 deletions packages/block-library/src/site-title/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import clsx from 'clsx';
/**
* WordPress dependencies
*/
import { useDispatch, useSelect } from '@wordpress/data';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { __ } from '@wordpress/i18n';
import {
Expand All @@ -26,46 +26,34 @@ export default function SiteTitleEdit( {
setAttributes,
insertBlocksAfter,
} ) {
const { level, levelOptions, textAlign, isLink, linkTarget } = attributes;
const { canUserEdit, title } = useSelect( ( select ) => {
const { canUser, getEntityRecord, getEditedEntityRecord } =
select( coreStore );
const canEdit = canUser( 'update', {
kind: 'root',
name: 'site',
} );
const settings = canEdit ? getEditedEntityRecord( 'root', 'site' ) : {};
const readOnlySettings = getEntityRecord( 'root', '__unstableBase' );

return {
canUserEdit: canEdit,
title: canEdit ? settings?.title : readOnlySettings?.name,
};
}, [] );
const { editEntityRecord } = useDispatch( coreStore );

function setTitle( newTitle ) {
editEntityRecord( 'root', 'site', undefined, {
title: newTitle,
} );
}
const { content, level, levelOptions, textAlign, isLink, linkTarget } =
attributes;
const canUserEdit = useSelect(
( select ) =>
select( coreStore ).canUser( 'update', {
kind: 'root',
name: 'site',
} ),
[]
);

const TagName = level === 0 ? 'p' : `h${ level }`;
const blockProps = useBlockProps( {
className: clsx( {
[ `has-text-align-${ textAlign }` ]: textAlign,
'wp-block-site-title__placeholder': ! canUserEdit && ! title,
'wp-block-site-title__placeholder': ! canUserEdit && ! content,
} ),
} );

const siteTitleContent = canUserEdit ? (
<TagName { ...blockProps }>
<RichText
tagName={ isLink ? 'a' : 'span' }
href={ isLink ? '#site-title-pseudo-link' : undefined }
aria-label={ __( 'Site title text' ) }
placeholder={ __( 'Write site title…' ) }
value={ title }
onChange={ setTitle }
value={ content }
onChange={ ( value ) => setAttributes( { content: value } ) }
allowedFormats={ [] }
disableLineBreaks
__unstableOnSplitAtEnd={ () =>
Expand All @@ -80,12 +68,12 @@ export default function SiteTitleEdit( {
href="#site-title-pseudo-link"
onClick={ ( event ) => event.preventDefault() }
>
{ decodeEntities( title ) ||
{ decodeEntities( content ) ||
__( 'Site Title placeholder' ) }
</a>
) : (
<span>
{ decodeEntities( title ) ||
{ decodeEntities( content ) ||
__( 'Site Title placeholder' ) }
</span>
) }
Expand Down
8 changes: 4 additions & 4 deletions packages/block-library/src/site-title/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
* @return string The render.
*/
function render_block_core_site_title( $attributes ) {
$site_title = get_bloginfo( 'name' );
if ( ! $site_title ) {
if ( ! isset( $attributes['content'] ) ) {
return;
}

$tag_name = 'h1';
$classes = empty( $attributes['textAlign'] ) ? '' : "has-text-align-{$attributes['textAlign']}";
$site_title = $attributes['content'];
$tag_name = 'h1';
$classes = empty( $attributes['textAlign'] ) ? '' : "has-text-align-{$attributes['textAlign']}";
if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
$classes .= ' has-link-color';
}
Expand Down
2 changes: 2 additions & 0 deletions packages/editor/src/bindings/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { registerBlockBindingsSource } from '@wordpress/blocks';
*/
import patternOverrides from './pattern-overrides';
import postMeta from './post-meta';
import site from './site';

/**
* Function to register core block bindings sources provided by the editor.
Expand All @@ -22,4 +23,5 @@ import postMeta from './post-meta';
export function registerCoreBlockBindingsSources() {
registerBlockBindingsSource( patternOverrides );
registerBlockBindingsSource( postMeta );
registerBlockBindingsSource( site );
}
70 changes: 70 additions & 0 deletions packages/editor/src/bindings/site.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { store as coreDataStore } from '@wordpress/core-data';

/**
* Gets a list of site fields with their values and labels
* to be consumed in the needed callbacks.
*
* @param {Object} select The select function from the data store.
* @return {Object} List of post meta fields with their value and label.
*/
const supportedFields = { title: { label: __( 'Title' ), type: 'string' } };
function getSiteFields( select ) {
const { getEditedEntityRecord } = select( coreDataStore );

const entityValues = getEditedEntityRecord( 'root', 'site', undefined );
const siteFields = {};
Object.entries( supportedFields ).forEach( ( [ key, props ] ) => {
// Don't include footnotes or private fields.
siteFields[ key ] = {
label: props.title || key,
value: entityValues?.[ key ],
type: props.type,
};
} );
return siteFields;
}

export default {
name: 'core/site',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this a generic "entity edit" binding where the connection would provide a "kind, name, property/key"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need a matching server implementation. I guess REST API resources map fairly close to entities, but I'm wondering if there will be edge cases and whether there might need to be a translation layer.

I'll put it on the TODO list. 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic entity edit sounds great on the client as a source for Block Bindings. Great idea. 👏

The specific details would need to be explored like validation whether all of them fit into the model, does the current user has permission to view or edit them. It wasn’t simple for Post Meta alone.

The server part for source registration might be a fun challenge, too. However there is so much potential 😀

getValues( { select, bindings } ) {
const metaFields = getSiteFields( select );

const newValues = {};
for ( const [ attributeName, source ] of Object.entries( bindings ) ) {
// Use the value, the field label, or the field key.
const fieldKey = source.args.key;
const { value: fieldValue, label: fieldLabel } =
metaFields?.[ fieldKey ] || {};
newValues[ attributeName ] = fieldValue ?? fieldLabel ?? fieldKey;
}
return newValues;
},
setValues( { dispatch, bindings } ) {
const newValues = {};
Object.values( bindings ).forEach( ( { args, newValue } ) => {
newValues[ args.key ] = newValue;
} );

dispatch( coreDataStore ).editEntityRecord(
'root',
'site',
undefined,
newValues
);
},
canUserEditValue( { select, args } ) {
if ( ! supportedFields[ args.key ] ) {
return false;
}

// Check that the user has the capability to edit post meta.
return select( coreDataStore ).canUser( 'update', {
kind: 'root',
name: 'site',
} );
},
};
1 change: 1 addition & 0 deletions test/integration/fixtures/blocks/core__site-title.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"name": "core/site-title",
"isValid": true,
"attributes": {
"content": "",
"level": 1,
"levelOptions": [ 0, 1, 2, 3, 4, 5, 6 ],
"isLink": true,
Expand Down
Loading