Add support for linkable column/cover/group blocks
Published: – Leave a comment Last update:
Currently, the only way to add links to content in the block editor is using text links, buttons or links on images. But what if you need more? In my case, the request was to be able to link whole columns, cover blocks and group blocks.
Fortunately, JavaScript is very powerful for such things and so I created linkable columns, covers and groups. There are three key tasks to do:
- Allow adding links in the backend for the mentioned blocks.
- Add the necessary code in the frontend to make the links available.
- Add the JavaScript to make the links actually work.
Prerequisites
I assume that you’re already familiar with the block editor itself as well as having a working compiling/minifying process for your block’s JSX to JavaScript.
Adding settings to backend
I used the filter blocks.registerBlockType
to add the necessary setting attributes as well as editor.BlockEdit
to add the actual settings. I created three settings: the first one is a text input for the link itself (if I had to rewrite it today, I would rather go with a toolbar as the default link functionality does), then a setting for automatically link to the media instead of a custom link (only available for cover blocks) as well as a setting to open the link in the current or a new tab.
The code looks like this:
import assign from 'lodash.assign';
import { InspectorControls } from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
TextControl,
ToggleControl,
} from '@wordpress/components';
import { createHigherOrderComponent } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';
const changeAttributes = ( settings, name ) => {
if ( name !== 'core/column' && name !== 'core/cover' && name !== 'core/group' ) {
return settings;
}
settings.attributes = assign( settings.attributes, {
link: {
type: 'string',
},
linkMedia: {
default: false,
type: 'boolean',
},
target: {
enum: [
'',
'tab',
],
type: 'string',
},
} );
return settings;
}
addFilter( 'blocks.registerBlockType', 'my/block-link-attributes', changeAttributes );
const changeControls = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
if ( props.name !== 'core/column' && props.name !== 'core/cover' && props.name !== 'core/group' ) {
return <BlockEdit { ...props } />;
}
const {
attributes: {
link,
linkMedia,
target,
},
setAttributes,
name,
} = props;
return (
<>
<BlockEdit { ...props } />
<InspectorControls>
<PanelBody
title={ __( 'Link Settings', 'textdomain' ) }
initialOpen={ !! link || !! target }
>
{ ! linkMedia
? <TextControl
label={ __( 'Link', 'textdomain' ) }
onChange={ link => setAttributes( { link } ) }
value={ link }
/>
: null
}
{ name === 'core/cover'
? <ToggleControl
checked={ !! linkMedia }
label={ __( 'Link to Media File', 'textdomain' ) }
onChange={ linkMedia => setAttributes( { linkMedia } ) }
/>
: null
}
<SelectControl
label={ __( 'Link opens in', 'textdomain' ) }
onChange={ target => setAttributes( { target } ) }
options={ [
{ label: __( 'Current Tab', 'textdomain' ), value: '' },
{ label: __( 'New Tab', 'textdomain' ), value: 'tab' },
] }
value={ target }
/>
</PanelBody>
</InspectorControls>
</>
);
};
}, 'changeControls' );
addFilter( 'editor.BlockEdit', 'my/block-link-controls', changeControls, 5 );
Code language: JavaScript (javascript)
Add frontend HTML code
We then now need a little bit of HTML in the frontend. So we use the render_block_{$this->name}
filter to add the necessary content. It’s two attributes (the one to the link or media and the target). Additionally, a new button will be added for users that need to navigate through the page with the keyboard (for accessibility reasons).
/**
* Add linkable block data.
*
* @param string $block_content The block content
* @param array $block Block data
* @return string The updated block content
* @throws \DOMException
*/
function my_add_linkable_block_data( string $block_content, array $block ): string {
$args = wp_parse_args(
$block['attrs'],
[
'link' => '',
'linkMedia' => false,
'target' => '',
]
);
libxml_use_internal_errors( true );
$dom = new DOMDocument();
$dom->loadHTML(
mb_convert_encoding(
'<html>' . $block_content . '</html>',
'HTML-ENTITIES',
'UTF-8'
),
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
);
// while group blocks is not always divs, we explicitly allow it on divs
foreach ( $dom->getElementsByTagName( 'div' ) as $element ) {
if ( $args['link'] || $args['linkMedia'] ) {
$element->setAttribute( 'class', $element->getAttribute( 'class' ) . ' is-linkable-block' );
}
if ( $args['link'] ) {
$element->setAttribute( 'data-link', $args['link'] );
$element->setAttribute( 'data-target', $args['target'] );
}
if ( $args['linkMedia'] ) {
if ( isset( $block['attrs']['url'] ) ) {
$element->setAttribute( 'data-link', $block['attrs']['url'] );
}
else {
$element->setAttribute( 'data-link', '' );
}
$element->setAttribute( 'data-target', $args['target'] );
}
// create an additional button for accessibility
if ( $args['link'] || $args['linkMedia'] ) {
$link = $args['link'] ?: $args['url'];
$accessibility_button = $dom->createElement( 'button' );
$accessibility_button->setAttribute( 'class', 'button my-block__link-button screen-reader-text' );
$accessibility_button->setAttribute( 'data-link', $link );
$accessibility_button->appendChild( $dom->createTextNode( esc_html__( 'Open Link', 'uscale' ) ) );
$element->insertBefore( $accessibility_button, $element->firstChild ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
}
}
$block_content = $dom->saveHTML( $dom->documentElement ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
libxml_use_internal_errors( false );
return str_replace(
[
'<html>',
'</html>',
],
[
'',
'',
],
$block_content
);
}
add_action( 'render_block_core/column', 'my_add_linkable_block_data', 10, 2 );
add_action( 'render_block_core/cover', 'my_add_linkable_block_data', 10, 2 );
add_action( 'render_block_core/group', 'my_add_linkable_block_data', 10, 2 );
Code language: PHP (php)
Add click functionality
To make the links work, we also need to add a small JavaScript snippet that handles the click events on these elements, which are identified by the class name is-linkable-block
(which has been added by the PHP function before).
/**
* Accessible links to whole blocks.
*/
document.addEventListener( 'DOMContentLoaded', () => {
const blocks = document.querySelectorAll( '.is-linkable-block' );
if ( ! blocks ) {
return;
}
for ( const block of blocks ) {
block.addEventListener( 'click', clickBlock );
}
/**
* Click on a block.
*
* @param {Event} event The event
*/
function clickBlock( event ) {
const target = event.currentTarget.getAttribute( 'data-target' ) || '';
switch ( target ) {
case 'tab':
window.open( event.currentTarget.getAttribute( 'data-link' ), '_blank' );
break;
default:
window.open( event.currentTarget.getAttribute( 'data-link' ), '_self' );
break;
}
}
} );
Code language: JavaScript (javascript)
Add CSS
Last but not least, to mark the element as linkable, we add a pointer cursor to it and for the accessible button, we need to set the position of the block to relative
. If your theme doesn’t support the default screen-reader-text
class, you need additional CSS regarding the accessible button.
.is-linkable-block {
cursor: pointer;
position: relative;
}
Code language: CSS (css)