WordPress 既存のテーマでコアブロックを拡張する
このサイトはそもそもオリジナルのテーマで開発しているため、通常のデフォルトブロックがさみしいもので、イマイチ機能的に優れているわけではなかったため、そろそろ既存のブロックを拡張した開発がしたいと考えました!!
今回はデフォルトブロックの拡張を目的として備忘録として残しておきます
前回シリーズの記事です
今回は特に、現在の私のポートフォリオサイトのカスタマイズとしてのアップデート作業になります。
したがって、備忘録的な内容になりがちなのでご了承下さい
まずは要件定義から書きだしてみましょう
今回の実装概要
今回実装するコアブロックです
ブロックの追加機能
- 見出し、段落、リストブロックに対してカスタムする
- 「マージン設定」として余白を追加できるようにする
- 前後アイコンを追加。Font Awesomeを使用しアイコンを含む<div>タグでラップする
以上の要件を想定して実装してみたいと思います
今回は JavaScriptとして用意されているアクションフックを使用します
フィルターを利用し現在のコアブロックを上書きする方法で実装してみます。
Block hook (アクションフック)
- blocks.registerBlockType
- このフックはエディター起動時に一度だけ実行され、ブロックを登録する際に、ブロックの設定をフィルタリングするために使用します。ブロック設定、登録されたブロックの名前、NULLまたは非推奨のブロック設定(登録された非推奨に適用される場合)を引数として受け取ります。<br>つまり全てのブロックの登録時のフックであり、そのタイミングで今回は属性値を登録します
- editor.BlockEdit
- ブロックの編集コンポーネントを変更するために使用します。元のブロックのBlockEditコンポーネントを受け取り、新しいラップされたコンポーネントを返します。
- editor.BlockListBlock
- ブロックの編集コンポーネントとすべてのツールバーを含む、ブロックのラッパーコンポーネントを変更するために使用されます。元の BlockListBlock コンポーネントを受け取り、新しいラップされたコンポーネントを返します。
- blocks.getSaveContent.extraProps
- save 関数ですべてのブロックに適用されるフィルタです。このフィルタは、save 関数のルート要素に追加のpropsを追加するために使用されます。たとえば、className、id、などこの要素に有効なpropsを追加します。<br><br>このフィルタは、現在の保存要素のプロップス、ブロックタイプ、ブロック属性を引数として受け取ります。そして、propsオブジェクトを返す必要があります。
これらのアクションフックを使用し以下の流れで実装していこうと思います
- フックするコアブロックを定義しておく
- コアブロックのattributesに任意の属性値を設定
- ブロックのインスペクターに独自の設定項目を追加する
- コアブロックの要素を受け取って任意のタグでラップする
- フロントエンドで反映されるように設定値をフックにて登録
- 各種スタイリング
それでは実践で実装していきます(^^)
フックするコアブロック
まずはフックするコアブロックを定義しようと思います
というのも、これから使用するブロックフィルターメソッドはエディターのの読み込み時に一度だけ実行されます。そして対象を指定しなければ全てのブロックに作用してしまう為、
どのブロックに作用するかの条件を指定する関数を作成します
const isValidBlockType = (name) => { const validBlockTypes = [ "core/paragraph", // 段落 "core/heading", //見出し ]; return validBlockTypes.includes(name); };
ブロック名を受け取って、指定したブロック名であれば true
になるように includes()
メソッドで判定します
attributesに任意の属性値を設定
次に前項で作成した関数を使用してコアブロックに attributes
を追加するためのフックを登録します
hookするのはblocks.registerBlockTypeです
import { addFilter } from "@wordpress/hooks"; // コアブロックにカスタム属性値を追加 function addAttribute(settings) { if (isValidBlockType(settings.name)) { settings.attributes = Object.assign(settings.attributes, { bottomSpace: { type: "string", default: "", }, frontIcon: { type: "string", default: "", }, endIcon: { type: "string", default: "", }, }); } return settings; } addFilter("blocks.registerBlockType", "oja/add-attr", addAttribute);
blocks.registerBlockTypeで登録されたこの関数は実行時に引数としてsettingsを受け取ります
このsettingsとはブロックを登録する際に使用したregisterBlockType
の第2引数で指定するパラメータです
そして処理内容ですが、settingsの属性値である attributes
に対してObject.assignメソッドでオブジェクト同士をマージ(結合)することで、独自の属性値を追加しております
今回定義した属性値は「マージン」と「前後アイコン」の3つ全てstringで定義しました
次はこれらの属性値に対してユーザーが操作できるようにするためのコントロールを追加します
ブロックのインスペクターに独自の設定項目を追加する
ブロックの編集オブジェクト、つまりregisterBlockType
のeditメソッドにフックするためにはeditor.BlockEdit
にフックします
このフックはブロックの編集コンポーネントを受け取り、これを新たにラップして返す事ができます
import { createHigherOrderComponent } from "@wordpress/compose"; import { Fragment } from "@wordpress/element"; import { InspectorControls } from "@wordpress/block-editor"; import { PanelBody, PanelRow, SelectControl, RadioControl, } from "@wordpress/components"; import { AwesomeIcons } from "./modules/AwesomeIcons"; const addBlockControl = createHigherOrderComponent((BlockEdit) => { return (props) => { const { setAttributes, isSelected, attributes,name } = props; const { bottomSpace, frontIcon, endIcon } = attributes; // isValidBlockType で指定したブロックが選択されたら表示 if (isValidBlockType(name)) { return ( <Fragment> <BlockEdit {...props} /> {isSelected && ( <InspectorControls> <PanelBody title="カスタム設定" initialOpen={false} className="oja-coreblock-controle" > <PanelRow> <RadioControl label="下部の余白設定" selected={bottomSpace} options={[ { label: "なし", value: "0" }, { label: "小", value: "3%" }, { label: "中", value: "6%" }, { label: "大", value: "10%" }, ]} onChange={(changeOption) => { setAttributes({ bottomSpace: changeOption }); }} /> </PanelRow> <SelectControl label="前部アイコン" value={frontIcon} options={AwesomeIcons} onChange={(frontIcon) => setAttributes({ frontIcon })} /> <SelectControl label="後部アイコン" value={endIcon} options={AwesomeIcons} onChange={(endIcon) => setAttributes({ endIcon })} /> </PanelBody> </InspectorControls> )} </Fragment> ); } return <BlockEdit {...props} />; }; }, "addBlockControl"); addFilter("editor.BlockEdit", "oja/block-control", addBlockControl);
まず、目につく記述はcreateHigherOrderComponent
です
これはReactの高階コンポーネントの概念であり、つまりコアブロックのもともとのBlockEdit
編集要素を受け取って新たに内容を追加してラップすることでその内容をマージ(結合)することができます
マージンを設定するRadioControl
RadioControlをインポートしてきてマージンの設定に使用します。
値として用意するのは、インラインCSS用の数値を値として渡して属性値に追加します
onChange={(changeOption) => {
setAttributes({ bottomSpace: changeOption });
}}
前後のアイコンを設定するSelectControl
SelectControlではアイコンを選択してもらいセットします
options={AwesomeIcons}
onChange={()=> setAttributes} //前後それぞれの属性値へセット
おっと、アイコン用の配列を用意するのを忘れていました、、文頭でインポートする「AwesomeIcon,js」を作成して配列を作成、エクスポートしておきましょう
/modules/AwesomeIcons.js
export const AwesomeIcons = [ { label: "なし", value: "" }, { label: "電球", value: "fa-regular fa-lightbulb" }, { label: "お知らせ", value: "fa-regular fa-bell" }, { label: "カート", value: "fa-solid fa-cart-shopping" }, { label: "吹き出し", value: "fa-regular fa-comment-dots" }, { label: "星", value: "fa-regular fa-star" }, { label: "はてなマーク", value: "fa-regular fa-question" }, { label: "チェック", value: "fa-regular fa-circle-check" }, { label: "ノート", value: "fa-solid fa-file-pen" }, { label: "クリップボード", value: "fa-regular fa-clipboard" }, { label: "鉛筆", value: "fa-solid fa-pen" }, { label: "歯車", value: "fa-solid fa-gear" }, { label: "注意", value: "fa-solid fa-triangle-exclamation" }, { label: "いいね!", value: "fa-regular fa-thumbs-up" }, { label: "低評価", value: "fa-regular fa-thumbs-down" }, { label: "ハート", value: "fa-regular fa-heart" }, { label: "旗", value: "fa-regular fa-flag" }, { label: "ビックリマーク", value: "fa-solid fa-circle-exclamation" }, ];
valueの値として設定しているのは本家「Font Awesome」からHTMLのクラス名を拝借してきました
、、、理想は、、Font Awesome のアイコンを全てダウンロードしその中からランダムでそのアイコンを表示、、希望のアイコンがなかったら全てのアイコンの中から選択できるように実装することが目標でしたが、、アイコンを全て出力するためのいい方法が思いつかず、、今回断念しました。。。。
次回アプデでリベンジ!!
コアブロックの要素を受け取って任意のタグでラップする
使用するアクションフック名はeditor.BlockListBlockです
このフックはブロックのタグやブロックを覆う要素、またその内包するプロパティやツールバーの設定まで全て受け取って任意の要素を追加し返すことができます
先に紹介していたeditor.BlockEditフックと混同しやすいですが、あちらは編集コンポーネントのみを受け取るため、ブロック全体を改変する場合はこちらのフックを利用します
import classNames from "classnames" const withOjaWrapperProp = createHigherOrderComponent((BlockListBlock) => { return (props) => { const { attributes, className, name, isValid } = props; if (isValid && isValidBlockType(name)) { const { frontIcon, endIcon, bottomSpace } = attributes; const extraStyle = { marginBottom: bottomSpace ? bottomSpace : undefined, }; const iconElement = (icon) => { let iconClass; if(name === "core/heading") { return iconClass = `${icon} fa-5x`; } else { return iconClass = `${icon} fa-3x`; } } const extraClass = [ frontIcon.replace(/fa-/g, "").split(" ")[1], endIcon.replace(/fa-/g, "").split(" ")[1], ]; const wrapperProps = { ...props.wrapperProps, "data-fronticon": frontIcon.split(' ')[1], "data-endicon": endIcon.split(' ')[1] }; return ( <div className={classNames("oja-corewraper", extraClass)} style={extraStyle} > {frontIcon !== "" && <i className={iconElement(frontIcon)}></i>} <BlockListBlock {...props} className={className} wrapperProps={wrapperProps} /> {endIcon !== "" && <i className={iconElement(endIcon)}></i>} </div> ); } return <BlockListBlock {...props} />; }; }, "withOjaWrapperProp"); wp.hooks.addFilter( "editor.BlockListBlock", "oja/with-oja-wrapper-prop", withOjaWrapperProp );
引数としているBlockListBlockは対象のブロック要素そのものです
色々ごちゃごちゃしているのでreturnメソッドから解説していきます
ブロックのラップ要素の出力を変更する
return
は一種類のタグを返すのがルールであるため、今回はdivタグでラップしています
その中で設定しているクラス名ですがnpmライブラリのclassnames
を使用しているため注意して下さい
ライブラリをインストールした後、importして使用しています
import classNames from "classnames"
classnames
はクラス名のを結合するのに使用しています
ラップする要素に現在のアイコンの要素が識別出来るクラス名を付与することで、そのアイコンに習ったスタイル(CSS)を適用するために実装します
そのための変数を用意しています
const extraClass = [
frontIcon.replace(/fa-/g, "").split(" ")[1],
endIcon.replace(/fa-/g, "").split(" ")[1],
];
replace(/fa-/g, "")
はJavaScriptのメソッドで要素の置換を行う関数ですが空文字列で置き換えることで実質削除しています。さらに、split(" ")[1]
で空白で区切って配列へ変換し二番目の要素、、アイコンのクラス名のquestionやbellなどの識別名のみとしています
なぜこのようにするかと言うと、そのまま対象の要素に指定してしまうと疑似要素としてアイコンが出力してしまうためです
マージン設定を反映する
このラップするdivタグに対してマージン設定を反映させます
style属性を追加しattributesの属性値の有無を三項演算子で分岐して設定しています
const extraStyle = {
marginBottom: bottomSpace ? bottomSpace : undefined,
};
アイコンを表示する
それではアイコンを出力する部分の解説です
{frontIcon !== "" && <i className={iconElement(frontIcon)}></i>}
属性値としてセットしたfrontIcon
が空でなければ「Font Awesome」のHTMLタグを出力していますさらにiconElement()
メソッドを定義し、対象の要素が「見出し」の場合と「段落」の場合でアイコンのサイズを変更できるようにクラス名を分岐しています
元のブロックにデータ属性を追加
元のブロックにパラメータを追加する理由はそのパラメータに対してCSSを設定できるからであります
ラップ要素もそうですが、内包する子要素に変化があった際にCSSではそれを取得する方法がないため、後で何か追加する際にも便利です
元のブロック要素であるBlockListBlockに追加のパラメータとしてwrapperProps
としてプロパティを渡しています。
const wrapperProps = {
...props.wrapperProps,
"data-fronticon": frontIcon.split(' ')[1],
"data-endicon": endIcon.split(' ')[1]
};
まず元のpropsには変更を加えないようにスプレッド構文(…)で変数展開しておきます
そして、アイコンの値に応じて「data属性」を追加しています
フロントエンド用コアブロックのpropsを設定する
ここからはフロントエンド側の表示に関する設定です
まだこのままでは、エディター上では問題なく変更も効くしCSSを当てればスタイル変更も効きますが、これを保存して、公開したりプレビューしたりするにはここから先の設定が必要です
既存のコアブロックにdata属性やclass名などを追加する場合の登録するフックはblocks.getSaveContent.extraPropsとなります
結論、今回の実装にはこのフックはあまり必要なかったように思います、、
このフックは既存のコアブロックのpropsを書き換える事です
この点においてこれからのブロック開発に有用だと感じたので積極的に使用してみます
function addSaveProps(extraProps, blockType, attributes ) { if (isValidBlockType(blockType.name)) { const {frontIcon, endIcon} = attributes; const wrapperProps = { ...extraProps.wrapperProps, "data-fronticon": frontIcon.split(" ")[1], "data-endicon": endIcon.split(" ")[1], }; return Object.assign(extraProps,{ ...extraProps, ...wrapperProps }); } return extraProps; } addFilter( "blocks.getSaveContent.extraProps", "oja/add-props", addSaveProps );
先のブロックエディットでも使用していた方法ですが、Object.assignでオブジェクト同士をマージすることでdata属性値を追加してみました。
extraProps
にコアブロックのpropsが格納されているというわけですね
phpのフックでフロントエンドをレンダリングする
前章で解説していたフックはブロックのpropsや属性値などはフックすることができたのですが、そのブロックに対して新たに別のHTMLタグでラップするには別のフックで登録する必要がありそうです
JSのフックで登録するとエラーが出る
ここからは失敗した内容の記録ですが、役に立てばと思い残しておきます
コアブロックを別のタグでラップするべく公式のリファレンスを見ていたら、blocks.getSaveElementというフックを見つけました。以下は引用です、、
上記は公式のリファレンスの日本語約です。
まさに私のやりたい内容であり、このフックを使用し以下のような記述でフロントエンドのレンダリングを試みました
以下はエラーになります
function modifyGetSaveElement(element, blockType, attributes) { if (!element) { return; } let getBlocks = wp.data.select("core/editor").getBlocks(); // 現在のブロックが現在の記事/ページで使用されていることを確認 if ( isValidBlockType(blockType.name) && getBlocks.find((block) => isValidBlockType(block.name)) ) { const { frontIcon, endIcon, bottomSpace } = attributes; const extraStyle = { marginBottom: bottomSpace ? bottomSpace : undefined, }; const iconElement = (icon) => { let iconClass; if (blockType.name === "core/heading") { return (iconClass = `${icon} fa-5x`); } else { return (iconClass = `${icon} fa-3x`); } }; const extraClass = [ frontIcon.replace(/fa-/g, "").split(" ")[1], endIcon.replace(/fa-/g, "").split(" ")[1], ]; return ( <div className={classNames("oja-corewraper", extraClass)} style={extraStyle} > {frontIcon !== "" && <i className={iconElement(frontIcon)}></i>} {element} {endIcon !== "" && <i className={iconElement(endIcon)}></i>} </div> ); } return element; } addFilter( "blocks.getSaveElement", "oja/modify-get-save-element", modifyGetSaveElement );
上記のコードを実行すると一応予定通りブロック要素を目的のタグでラップすることができました
しかしエディター側でブロック要素にエラーが出ており、リカバリーで復活させることは出来るが、再読み込みするたびにエラーになるため実用には至らず、、、
Block validation: Block validation failed for `core/heading
エラーの内容としてはブロックの検証エラーとのことで、本来このブロックは「pタグ」が保存されそれをレンダリングするはずが、ブロック要素をラップしたことで本来期待されるタグが保存されておらず検証時にエラーとなるようです
これには公式にも言及してあり、どういうわけかこのフックの前の章で使用していたblocks.getSaveContent.extraPropsの欄に以下がありました
ということで、公式にあるようにサーバーサイド(php)でrender_blockフックを使用してみましょう
render_blockフックをfunctions.phpで登録
render_blockの公式リンクです
また英語ですがこちらの記事も参考になりました
Adding a DIV wrapper to Gutenberg’s Classic block
このブロックは登録も実装もテーマで行っているため、レンダリング用の関数はテーマの「functions.php」に記述していくようにします
私の場合は各項目を別ファイルに用意してrequire_once()
しております
require_once locate_template('lib/blocks_render/core_expantion.php'); //カスタムブロックレンダリング用
それではレンダリング関数を記述してみます
core_expantion.php (functions.php)
<?php // ラップエレメントにclass名を追加する function wrap_classname ($f_icon, $e_icon) { $class_name = "oja-corewraper "; if (isset($f_icon)) { $class_name .= ' ' . explode(" ",str_replace('fa-', '', $f_icon))[1]; } if (isset($e_icon)) { $class_name .= ' ' . explode(" ",str_replace('fa-', '', $e_icon))[1]; } return $class_name; } // マージン設定を反映する function margin_style ($bottom_space) { if(empty($bottom_space)) return null; $style = 'style="'; $style .= 'margin-bottom:' . $bottom_space; $style .= '"'; return $style; }; // アイコン判定出力 function icon_render($icon, $block_name) { if(empty($icon)) return null; if($block_name === "core/heading") { return '<i class="' . $icon .' fa-4x"></i>'; } if($block_name === "core/paragraph") { return '<i class="' . $icon .' fa-3x"></i>'; } } function core_expantion_render( $block_content, $block ) { //段落と見出しのみに追加する if ( $block['blockName'] === 'core/paragraph' || $block['blockName'] === "core/heading" ) { $front_icon = @$block['attrs']['frontIcon']; $end_icon = @$block['attrs']['endIcon']; $bottom_space = @$block['attrs']['bottomSpace']; $content = '<div class="' . wrap_classname($front_icon, $end_icon). '"' . margin_style(@$bottom_space) .'>'; $content .= icon_render($front_icon, $block['blockName']); $content .= $block_content . icon_render($end_icon, $block['blockName']) . '</div>'; return $content; } return $block_content; } if(!is_admin()) { add_filter( 'render_block', 'core_expantion_render', 10, 2 ); }
見ずらい、、ですが解説してみます、
render_blockの引数を確認する
render_blockフックでは2つ引数を受け取ることができ、$block_content
にはそのブロックの要素が格納されています。つまりこの引数に対して、任意のタグでラップすることで目的の実装ができそうです。
もう一つの引数$block
にはブロックのデータが格納されています。propsや属性値などはこちらにあります
これらの要素を用いて管理画面と同じように要素を構成し、returnするところまでがゴールです
ブロックデータを変数化する
ブロックに登録した設定データを変数にして使いやすく視認性を確保したいのですが、そのまま代入してしまうとphpの警告メッセージ「”Undefined variable”」が出てしまいます
これは変数や配列がまだ未定義の場合に代入しようとした場合に発生しますが、セッションのデータや、今回のようにをカスタマイズを適用するための変数を設定するかどうかわからない場合にも、他の設定していないブロックから警告が出てしまいます
そこで使用するのが@マークです
@マークを変数や配列の先頭に付けることで、もし変数が未定義の場合には空文字列「””」が代入されることになります
$name = @$_POST['name']; //未定義の場合は""になる
ラップ要素のクラス名を設定する
まずやることとしてはブロックを覆う要素に対してクラス名を動的に付与したいと思い関数を用意しました
wrap_classname()はアイコン名を引数として2つ受け取りそれぞれ添削しクラス名に追加する関数です。phpの組み込み関数を使用していますが、explode()
はJS版ではsplit()
でしたね。
これらの関数は区切り文字で分割し配列化しますstr_replace()
はJS版ではreplace()
メソッドでした。こちらは任意の要素で文字列を置換することで実質的に文字列を削除しています
またここでは変数が未定義の場合の対策としてisset()
関数を使用しています
この関数は引数として渡した変数が未定義ではない場合にtrueを返してくれます
ラップ要素にインラインスタイルを適用する
margin_style()
関数は今回作成したマージン設定を反映させる関数となります
処理内容は渡された変数の存在の有無を確認し、設定がされていればインラインスタイルの文字列をリターンするといった内容です
こうして関数化しておくことで視認性の確保と、もし何か他に処理を追加したくなったとしても引数とスタイルを追加するだけで出力するのが楽になるからです
アイコンが存在するか判定し出力する
機能面の最後の項目です。 icon_render()
はアイコン要素とブロックの名前を受け取り、ブロック名で条件判定し「Font Awesome」のHTML要素を返します
もちろんアイコンの要素が存在しなければ即リターンで処理を終わらせます
SCSSでスタイリング
最後に出力したタグやdata属性値にに対してSCSSでスタイリングを整えます
もちろんお好みでオリジナリティあふれるSCSSに書き換えることをおすすめします
@charset 'utf-8'; @mixin iconColorSet($color) { background-color: rgba($color, 0.1); i { color: $color; } } .oja-corewraper { display: flex; flex-wrap: wrap; width: 100%; margin-left: auto; margin-right: auto; max-width: 840px; align-items: baseline; justify-content: flex-start; padding: 12px; i { margin: 0 15px; } & > *:not(i){ flex: 1; } p[data-fronticon] { padding-left: 0px; } p[data-endicon] { padding-right: 0px; } // 注意、ハート、?マーク、ビックリマーク &.triangle-exclamation, &.heart, &.question, &.circle-exclamation { @include iconColorSet(#F13D54); } // 星 、電球、カート、旗 &.star, &.lightbulb, &.cart-shopping, &.flag { @include iconColorSet(#FFBF0E); } // クリップボード、歯車、いいね!、低評価 &.clipboard, &.gear, &.thumbs-down, &.thumbs-up { @include iconColorSet(#616dc9); } // お知らせ、吹き出し、チェック、鉛筆、ノート &.bell, &.comment-dots, &.circle-check, &.pen, &.file-pen { @include iconColorSet(#05B483); } }
今回はこんな感じにアイコンと中のテキストを横並びにして、アイコンのカラーと背景色を揃えました
もう少し欲をかくならカラーパレットを追加してアイコンのカラーリングを選択してもらい、そのカラーリングを背景色や、中のコンテンツに反映させるというアップデートも面白そうです
今回の学び
やっとここまで来ました、、なんか通常のブロックを作成するよりフックを使用するぶんいくらか手間な感じがしましたが、コアブロックをカスタマイズできたというのはオリジナルテーマとしてはかなり嬉しく思いました
やはりいちばん困ったのは、blocks.getSaveElementによる検証エラーです
本文でも長々書きましたが、公式にあるフックだったので間違いないと鵜呑みにしていた節がありました。
エラーで行き詰まったのち、外国の記事なども読み漁り解決できそうな方法を見つけた後改めて公式の文言をよくよく見ると、その下のフックの説明欄の下に注意書きが、、、苦笑
またその後はPHPによる未定義の警告にも悩まされました、、
PHPも段々と厳格な言語になっていっているようで、ゆるい言語ばかり触ってきたのでこのようにプログラム側から「NO」と言われると困惑します
しかしより堅牢な成果物が生まれる期待や喜びもあるため、前向きに捉え楽しく調べることができております(^^)
あとは「Font Awesome」の一元フォルダ管理などを行い全てのアイコンをランダム表示するなど、有名テーマにありがちな設定ができれば最高ですね
お疲れさまでした(^^)
コメントをお待ちしております