WordPress Core Group Block - markhowellsmead/helpers GitHub Wiki

Adding a link control to the core/group block

JavaScript

/**
 * External dependencies
 */
import classnames from 'classnames';

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';
import { addFilter } from '@wordpress/hooks';
import { BlockControls, __experimentalLinkControl as LinkControl } from '@wordpress/block-editor';
import { Button, ToolbarButton, MenuGroup, MenuItem, Popover } from '@wordpress/components';
import { link, linkOff, page, Icon } from '@wordpress/icons';
import { useState } from '@wordpress/element';

/**
 * Add the attributes needed for linked groups.
 *
 * @param {Object} settings
 */
function addAttributes(settings) {
	if ('core/group' !== settings.name) {
		return settings;
	}

	// Add the link attributes.
	const linkAttributes = {
		href: {
			type: 'string',
		},
		linkDestination: {
			type: 'string',
		},
		linkTarget: {
			type: 'string',
		},
	};

	const newSettings = {
		...settings,
		attributes: {
			...settings.attributes,
			...linkAttributes,
		},
	};

	return newSettings;
}

addFilter('blocks.registerBlockType', 'enable-linked-groups/add-attributes', addAttributes);

/**
 * Filter the BlockEdit object and add linked group inspector controls.
 *
 * @todo Fix the issue where the popover remains open when clicking another block.
 *
 * @param {Object} BlockEdit
 */
function addInspectorControls(BlockEdit) {
	return (props) => {
		if (props.name !== 'core/group') {
			return <BlockEdit {...props} />;
		}

		const [isEditingURL, setIsEditingURL] = useState(false);
		const [popoverAnchor, setPopoverAnchor] = useState(null);
		const { attributes, setAttributes } = props;
		const { href, linkDestination, linkTarget } = attributes;

		return (
			<>
				<BlockEdit {...props} />
				<BlockControls group="block">
					<ToolbarButton
						ref={setPopoverAnchor}
						name="link"
						icon={link}
						title={__('Link', 'shp-linked-group-block')}
						onClick={() => setIsEditingURL(true)}
						isActive={!!href || linkDestination === 'post' || isEditingURL}
					/>
					{isEditingURL && (
						<Popover
							anchor={popoverAnchor}
							onClose={() => setIsEditingURL(false)}
							focusOnMount={true}
							offset={10}
							className="enable-linked-groups__link-popover"
							variant="alternate"
						>
							{linkDestination !== 'post' && (
								<LinkControl
									value={{
										url: href,
										opensInNewTab: linkTarget === '_blank',
									}}
									onChange={({ url: newURL = '', opensInNewTab }) => {
										setAttributes({
											href: newURL,
											linkDestination: newURL ? 'custom' : undefined,
											linkTarget: opensInNewTab ? '_blank' : undefined,
										});
									}}
									onRemove={() =>
										setAttributes({
											href: undefined,
											linkDestination: undefined,
											linkTarget: undefined,
										})
									}
								/>
							)}
							{!href && !linkDestination && (
								<div className="enable-linked-groups__link-popover-menu">
									<MenuGroup>
										<MenuItem
											icon={page}
											iconPosition="left"
											info={__('Use when the Group is located in a Query block.', 'shp-linked-group-block')}
											onClick={() =>
												setAttributes({
													linkDestination: 'post',
												})
											}
										>
											{__('Link to current post', 'shp-linked-group-block')}
										</MenuItem>
									</MenuGroup>
								</div>
							)}
							{linkDestination === 'post' && (
								<div className="enable-linked-groups__link-popover-post-selected">
									<div className="enable-linked-groups__link-popover-post-selected-label">
										<span className="enable-linked-groups__link-popover-post-selected-icon">
											<Icon icon={page} />
										</span>
										{__('Linked to current post', 'shp-linked-group-block')}
									</div>
									<Button
										icon={linkOff}
										label={__('Remove link', 'shp-linked-group-block')}
										onClick={() =>
											setAttributes({
												linkDestination: undefined,
											})
										}
									/>
								</div>
							)}
						</Popover>
					)}
				</BlockControls>
			</>
		);
	};
}

addFilter('editor.BlockEdit', 'enable-linked-groups/add-inspector-controls', addInspectorControls);

/**
 * Add linked group classes in the Editor.
 *
 * @param {Object} BlockListBlock
 */
function addClasses(BlockListBlock) {
	return (props) => {
		const { name, attributes } = props;

		if ('core/group' !== name) {
			return <BlockListBlock {...props} />;
		}

		const classes = classnames(props?.className, {
			'is-linked': attributes?.href || attributes?.linkDestination === 'post',
		});

		return <BlockListBlock {...props} className={classes} />;
	};
}

addFilter('editor.BlockListBlock', 'enable-linked-groups/add-classes', addClasses);

PHP

<?php

namespace SayHello\MustUsePlugin\Blocks\CoreGroup;

use DOMDocument;

class Block
{
	public function run()
	{
		add_action('render_block_core/group', [$this, 'addGroupLink'], 10, 2);
	}

	public function addGroupLink(string $content, array $block)
	{

		if (empty($content)) {
			return $content;
		}

		if (! isset($block['attrs']['href']) && ! isset($block['attrs']['linkDestination'])) {
			return $content;
		}

		$href             = $block['attrs']['href'] ?? '';
		$link_destination = $block['attrs']['linkDestination'] ?? '';
		$link_target      = $block['attrs']['linkTarget'] ?? '_self';
		$link_rel         = '_blank' === $link_target ? 'noopener noreferrer' : 'follow';

		$link = '';

		if ('custom' === $link_destination && $href) {
			$link = $href;
		} elseif ('post' === $link_destination) {
			$link = get_permalink();
		}

		if (!$link) {
			return $content;
		}

		$content = $this->convertStringEncoding($content);

		$document = new DOMDocument();
		libxml_use_internal_errors(true);
		$document->loadHTML($content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
		$block_outer = $document->documentElement;

		$css_classes = $block_outer->getAttribute('class');
		$css_classes .= ' wp-block-group--linked';

		$block_outer->setAttribute('class', $css_classes);

		$link_element = $document->createElement('a');
		$link_element->setAttribute('class', 'wp-block-group__link');
		$link_element->setAttribute('href', $link);
		$link_element->setAttribute('target', esc_attr($link_target));
		$link_element->setAttribute('rel', esc_attr($link_rel));
		$link_element->setAttribute('aria-hidden', 'true');
		$link_element->setAttribute('tabindex', '-1');
		$link_element->textContent = '';

		$block_outer->appendChild($link_element);

		$body = $document->saveHtml($document->getElementsByTagName('body')->item(0));
		return str_replace(['<body>', '</body>'], '', $body);
	}

	/**
	 * PHP 8.2-compatible string conversion.
	 * Formerly mb_convert_encoding($string, 'HTML-ENTITIES', 'UTF-8')
	 *
	 * @param string $string
	 * @param string $convert_to
	 * @return string
	 */
	private function convertStringEncoding(string $string, $convert_to = 'UTF-8')
	{
		return mb_encode_numericentity(
			htmlspecialchars_decode(
				htmlentities(
					$string,
					ENT_NOQUOTES,
					$convert_to,
					false
				),
				ENT_NOQUOTES
			),
			[0x80, 0x10FFFF, 0, ~0],
			$convert_to
		);
	}
}

(S)CSS

.wp-block-group {
	&.wp-block-group--linked {
		position: relative;
	}

	&__link {
		display: block;
		position: absolute;
		top: 0;
		right: 0;
		bottom: 0;
		left: 0;
		overflow: hidden;
		text-indent: 100%;
		white-space: nowrap;
		margin: 0 !important;
		max-width: none !important;
	}
}
⚠️ **GitHub.com Fallback** ⚠️