Gutenbergと呼ばれる、WordPressブロックエディタにコアブロックという標準の部品があります。プラグインを作成し、このコアブロックの部品に対してカスタマイズをしようとしました。しかし、実装方法を調査するものの、そのものズバリの方法が見つからず苦労しましたので、ここに記録します。
今回は工作はありません。
カスタマイズ内容
標準部品の「ボタン」と「画像」を対象とします。
ボタン部品を配置すると、以下のようなHTMLが生成されます。
<div class="wp-block-button"><a class="wp-block-button__link">test</a></div>
<a>タグに対して、属性(ここではdata-testとする)をユーザが任意に設定できるようにしたい。
<div class="wp-block-button"><a class="wp-block-button__link" data-test="testvalue">test</a></div>
画像に関しても<a>タグに対して属性を設定するのはボタンと同様ですが、画像部品に<a>タグはありませんので、<img>の外側に<a>を追加します。属性値が指定されない場合は、<a>は追加しません。
<img src="..." alt="" class="wp-image-17"/>
属性値が指定された場合のみ、下のようにします。
<a data-test="testvalue"><img src="..." alt="" class="wp-image-17"/></a>
メタデータを拡張
以下では「ボタン」部品についてのみ説明します。「画像」部品についてもほぼ同じ方法で実装しましたので省略します。
カスタマイズにより「ボタン」部品に対して設定できるパラメータがひとつ増えますので、メタデータ(block.json)を拡張する必要があります。オリジナルの「ボタン」部品のメタデータ定義抜粋は以下のようになります。全体はこちらで参照できます。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "core/button",
"title": "Button",
"category": "design",
"parent": [ "core/buttons" ],
"description": "Prompt visitors to take action with a button-style link.",
"keywords": [ "link" ],
"textdomain": "default",
"attributes": {
"url": {
"type": "string",
"source": "attribute",
"selector": "a",
"attribute": "href"
},
"title": {
"type": "string",
"source": "attribute",
"selector": "a",
"attribute": "title"
},
...(以下略)
attributesがボタンに設定できるパラメータ定義となります。url, title,…とありますが、これにtestというパラメータを追加します。
import { addFilter } from '@wordpress/hooks';
addFilter(
'blocks.registerBlockType',
'markerise/block-ext/register-block-type',
( settings, name ) => {
if ( name !== 'core/button' ) {
return settings;
}
settings.attributes = lodash.assign( settings.attributes, {
test: {
type: 'string',
source: 'attribute',
selector: 'a',
attribute: 'data-test',
},
} );
return settings;
}
);
上のフィルタ(blocks.registerBlockType)を設定することで、core/buttonのblock.jsonに対してパラメータ’test’が追加されます。testパラメータは文字列型で、<a>タグの属性data-testと連動します。
testパラメータの入力フォームを追加
新設したパラメータを設定するUIが必要です。今回はブロック設定に入力フォームを追加しました。フィルタ(editor.BlockEdit)を設定します。
import { InspectorControls } from '@wordpress/block-editor';
import {
__experimentalInputControl as InputControl,
PanelBody,
} from '@wordpress/components';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
addFilter(
'editor.BlockEdit',
'markerise/block-ext/block-edit',
createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
const { attributes, setAttributes, name, isSelected } = props;
if ( name !== 'core/button' || ! isSelected ) {
return <BlockEdit { ...props } />;
}
const [ value, setValue ] = useState( attributes.test );
return (
<>
<BlockEdit { ...props } />
<InspectorControls>
<PanelBody title={ __( 'テスト', 'markerise' ) }>
<InputControl
value={ value }
onChange={ ( nextValue ) => {
const val = nextValue ?? '';
setValue( val );
setAttributes( { test: val } );
} }
/>
</PanelBody>
</InspectorControls>
</>
);
};
}, 'addMrcBlockEditControl' )
);
上の記述で、オリジナルの<BlockEdit />に<InspectorControls />を追加しています。その結果、エディタでボタン部品を選択した場合に、下の図のようなフォームが追加されます。
HTML出力をカスタマイズ
追加したパラメータ値をHTMLに反映するために、以下のフィルタ設定が必要です。
addFilter(
'blocks.getSaveContent.extraProps',
'markerise/block-ext/save-content-extraprops',
( props, blockType, attributes ) => {
if ( blockType.name !== 'core/button' ) {
return props;
}
const { test } = attributes;
if ( test ) {
return lodash.assign( props, {
'data-test': test,
} );
}
return props;
}
);
blocks.getSaveContent.extraPropsフィルタを追加し、testパラメータがdata-test属性値に反映されるよう、propsを拡張します。
実装結果
ボタンを配置し、以下のように「テスト」パラメータに値を設定します。
すると、<a>タグにdata-test属性が追加されるはずですが…
なぜか<div>に属性が追加されてしまっています。しかも、いろいろさわっていると、
こんなエラーが出てしまいました。
エラーの原因
Gutenbergエディタでは、さまざまなタイミングでBlock Validationというチェックが動作します。部品定義とHTMLの内容に矛盾がないかがチェックされます。上記のエラーはメタデータの定義では<a>タグにdata-test属性が付与されるということになっているところ、HTMLでは<div>についてしまっていることが原因となります。
では、なぜ属性が<div>についてしまったのか?ですが、「ボタン」部品のオリジナルのソースコードを見ると分かります。
以下、save関数の抜粋を掲載します。全体はこちらです。
return (
<div { ...useBlockProps.save( { className: wrapperClasses } ) }>
<RichText.Content
tagName="a"
className={ buttonClasses }
href={ url }
title={ title }
style={ buttonStyle }
value={ text }
target={ linkTarget }
rel={ rel }
/>
</div>
);
}
<RichText.Content tagName=”a”…>で<a>タグを出力していますが、なんと、付与する属性は固定でカスタマイズの余地がありません。 せっかくメタデータ(block.json)の定義があるのに、この実装はヒドイと思いました。これではプラグイン形式の実装は諦め、WordPressのソースコードを直接変更するしかありません。
PHPが登場
WordPressの公式ドキュメントのblocks.getSaveContent.extraPropsフィルタの説明に以下のような記述がありました。
blocks.getSaveContent.extraPropsフィルタでエラーが出る場合はサーバサイド(つまりPHP)でなんとかしろということのようです。ここまでフロントエンドで実装をして、出力段階だけサーバサイドで細工するというのは納得いかないというか美しくないというか、釈然としませんが仕方なさそうです。
実装の方針を変更し、フロントエンド側では、属性値は<a>タグではなく<div>につけるようにします。上で掲載したメタデータの拡張コード(block.registerBlockTypeフィルタ)を以下のようにselector: ‘div’と変えます。
import { addFilter } from '@wordpress/hooks';
addFilter(
'blocks.registerBlockType',
'markerise/block-ext/register-block-type',
( settings, name ) => {
if ( name !== 'core/button' ) {
return settings;
}
settings.attributes = lodash.assign( settings.attributes, {
test: {
type: 'string',
source: 'attribute',
selector: 'div',
attribute: 'data-test',
},
} );
return settings;
}
);
これでメタデータの定義とフロントエンドで管理しているHTML出力に矛盾は無くなりますので、Validation Errorは解消します。
このままでは、<a>タグに属性をつけるという仕様とは違う結果となりますので、サーバサイドでフィルタを実施します。
if ( ! is_admin() ) {
add_filter( 'render_block', function( $block_content, $block ) {
if ( $block['blockName'] === 'core/button' ) {
if ( preg_match( '/(.+)data-test="([^"]+)"(.+)<a (.+)/', $block_content, $matches) ) {
return $matches[1] . $matches[3] . '<a data-test="' . $matches[2] . '" ' . $matches[4];
}
}
return $block_content;
}, 10, 2);
}
サーバサイドのPHPでrender_blockのフィルタを設定します。’core/button’のブロックに対して、<div>タグにdata-test属性がついている場合は、<a>タグに付け替えるという処理をしています。その結果、以下のようになりました。
エディタ部品のカスタマイズなのに、フロントエンドとサーバサイドにコードを記述しなければいけないのが釈然としませんが、他に方法はないようです。もしくは、コアブロックのカスタマイズは諦め、新規の部品として実装する方法になると思います。
それにしても、WordPressのコーディング規約にはなかなか慣れません。