WordPress・カスタムブロック作成の基本(その3)

目次
  1. カラーパレットについて
    1. テーマサポートについて
    2. ハイオーダーコンポーネント(withColors)
    3. ブロックのサポート機能
  2. 非推奨のバージョン追加について
  3. カスタムブロックパターン
  4. 書式設定ツールバーにカスタムボタンを設定
    1. @wordpress/scriptsを利用する
  5. Block Filters
  6. 関連投稿

カラーパレットについて

(その1)では、カラーパレットはインラインスタイルで対応しましたが、カスタムカラー以外は「class属性」で対応します

簡単な方法がありますが、あえてハイオーダーコンポーネントを理解するために「widthColors」をつかいます

テーマサポートについて

テーマには「add_theme_support」でオプトイン・オプトアウトできる機能があります
せっかくなので、色を登録しようと思います

  • エディターカラーパレット (色のデフォルトセット)
  • エディターテキストサイズパレット( テキストサイズのデフォルトセット)
  • レスポンシブな埋め込みコンテンツ (オプトインが必要)
  • フロントエンドとエディタのスタイル(コアのスタイルが適切であることを確認してオプトインするか、テーマに合う独自のスタイルを記述)
  • 「行の高さ」や「カスタムユニット」などいくつかのブロックツール
  • コアのブロックパターン をオプトアウトできます

テーマのfunctions.phpに「add_theme_support」で、カラーパレットに色を登録します

function hoge() {
//ここから追加
    add_theme_support( 'editor-color-palette', array(
        array(
//aria属性(ラベル)
            'name'  => esc_attr('strong magenta'),
//'slug'は色の一意の識別
            'slug'  => 'strong-magenta',
            'color' => '#a156b4',
        ),
        array(
            'name'  => esc_attr('light magenta'),
            'slug'  => 'light-magenta',
            'color' => '#d0a5db',
        ),
    ) );
//登録の数は制限なし 
}
add_action( 'after_setup_theme', 'hoge' );
カラーパレット
カラーパレットを表示した画面

*実際には「widthColors」は不要で簡単な方法(block.jsonの「supportsプロパティ」)があります
その場合でもカラーパレットに色を登録したときは、登録した色の数だけスタイルシートにCSSが必要です
クラス名は
has-[色の識別子]-background-color
has-[色の識別子]-color

.has-strong-magenta-background-color {
    background-color: #a156b4;
}
.has-strong-magenta-color {
    color: #a156b4;
}
.has-light-magenta-background-color {
    background-color: #d0a5db;
}
.has-light-magenta-color {
    color: #d0a5db;
}

ハイオーダーコンポーネント(withColors)

block.jsonファイル
「カスタムカラー」のための「customBroundColorとcustomTextColor」が必要です
*名前の変更不可

	"attributes": {
//省略
		"textColor": {
			"type": "string"
		},
		"backgroundColor": {
			"type": "string"
		},
		"customBroundColor": {
			"type": "string"
		},
		"customTextColor": {
			"type": "string"
		}
	},

1:「withColors 」をつかいます
ハイオーダーコンポーネントはコンポーネントのロジックを再利用するためのReactのテクニックで、あるコンポーネントを受け取って新規のコンポーネントを返すような関数です
2 :「save」では「getColorClassName」をつかいクラスを返すようにします

コードを見る

//ハイオーダーコンポーネントprops
edit: withColors
(function ({...props}) {
//コンソールで確認
	console.log(props)

	}
 ),
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import {
	useBlockProps,
	RichText,
	AlignmentToolbar,
	BlockControls,
	InspectorControls,
	PanelColorSettings,
	ContrastChecker,
//withColorsを追加
	withColors,
//getColorClassNameを追加
	getColorClassName
} from '@wordpress/block-editor';
import classnames from "classnames";
import { RangeControl, PanelBody } from "@wordpress/components";
import './style.scss';
import './editor.scss';

registerBlockType('create-block/test', {
//withColors
edit: withColors({	backgroundColor: 'backgroundColor', textColor: 'color',})
//editの値である関数をwithColorsでラップしwithColorsからpropsを受け取る
(function (props) {
//コンソールで確認
	console.log(props)
//withColorsのpropsにあわせてリファクタリング
	const { attributes, setAttributes,
		backgroundColor,
		textColor,
		setBackgroundColor,
		setTextColor } = props;
//backgroundColor, textColorは削除リファクタリング
	const { alignment, content, shadow, shadowOpacity} = attributes;
	const onChangeAlignment = ( newAlignment ) => {
		setAttributes( {
		alignment: newAlignment === undefined ? 'none' : newAlignment,
		} );
	};
	const blockProps =  useBlockProps( {
		className: classnames( {
			"has-shadow": shadow,
			[`shadow-opacity-${shadowOpacity * 100}`]: shadowOpacity,
//ついでにインラインにしていたalignmentのクラスを追加
			[`text-align-${alignment}`]: alignment,
		}),
//ついでにRichTextからstyle属性を削除、useBlockPropsに渡す
//textColor backgroundColorを追加
		style: {
			backgroundColor: backgroundColor.color,
			color: textColor.color,
		},
	});
 return (
	<>
{/* //InspectorControls,PanelColorSettings,ContrastCheckerを追加 */}
	<InspectorControls>
{/* //PanelBodyにRangeControlを追加0.1~0,4の値で0.1刻み */}
		<PanelBody title={"Setting"}>
{/* //shadowがtrueのときにRangeControlを表示 */}
		{shadow && (
			<RangeControl
				label={"Shadow Opacity"}
				value={shadowOpacity}
				onChange={( shadowOpacity ) => setAttributes( {shadowOpacity } )}
				min={0.1}
				max={0.4}
				step={0.1}
			/>
		)}
		</PanelBody>
		<PanelColorSettings
			title={"color"}
			colorSettings={[
				{
			//backgroundColor.color
					value: backgroundColor.color,
			//setBackgroundColor
					onChange: setBackgroundColor,
					label: "BackgorundColor"
				},
				{
			//textColor.color
					value: textColor.color,
			//setTextColor
					onChange: setTextColor,
					label: "TextColor"
				}
			]}
		>
{/* textColor backgroundColor変更*/}
			<ContrastChecker
				textColor={textColor.color}
				backgroundColor={backgroundColor.color}
			/>
		</PanelColorSettings>
	</InspectorControls>
	<BlockControls
		controls={[
		{
			icon: "chart-bar",
			title: "Shadow",
			onClick: () => {setAttributes({shadow: !shadow})},
			isActive: shadow
		}
		]}
		>
		<AlignmentToolbar
			value={ alignment }
			onChange={ onChangeAlignment }
		/>
	</BlockControls>
	<RichText  {...blockProps}
		allowedFormats={ [ 'core/bold', 'core/italic', 'core/link' ] }
		tagName="p"
		onChange={ ( content ) => setAttributes( { content } ) }
		value={content}
	/>
	</>
	);
	}
 ),

	save({ attributes }) {
//customBackgroundColor,customTextColorを追加
	const { alignment, content, backgroundColor, textColor, customBackgroundColor,
		customTextColor, shadow, shadowOpacity } = attributes;
//getColorClassNameでクラスを返します
	const backgroundClass = getColorClassName(
		'background-color',
		backgroundColor
	);
	const textClass = getColorClassName( 'color', textColor );

	const blockProps =  useBlockProps.save( {
		className: classnames( {
			"has-shadow": shadow,
			[`shadow-opacity-${shadowOpacity * 100}`]: shadowOpacity,
//backgroundClassと、textClass、ついでにインラインにしていたalignmentのクラスを追加
			[`text-align-${alignment}`]: alignment,
			[ textClass ]: textClass,
			[ backgroundClass ]: backgroundClass,
		}),
//ついでにRichTextからstyle属性を削除、useBlockPropsに渡す
//その他の色を使ったときはインラインスタイル
		style: {
			backgroundColor: backgroundClass
						? undefined
						: customBackgroundColor,
			color: textClass ? undefined : customTextColor,
		},
	});

return (
	<>
{/* blockPropsを展開 */}
	<RichText.Content { ...blockProps }
		tagName="p"
		value={content}
	/>
	</>
	);
	},

} );

「props」をコンソールで確認した画面です
「backgroundColor.colorとtextColor.color」が値になり「setBackgroundColorとsetTextColor」で値を更新します

コンソール画面を確認

propsをコンソールで確認

ブロックのサポート機能

簡単な方法です(カスタムカラー以外を「class属性」で対応)
今までの「withColorsやgetColorClassName」は不要
それどころか「PanelColorSettings(カラーパレット)」「style属性・class属性」も不要です

block.jsonの「supportsプロパティ」は
機能にオプトインすると、ブロックに追加の属性が登録され、属性を操作するUIが提供されます

「block.jsonファイル」のプロパティの「supports」に「color」を追加するだけで、カラーパレット機能がサポートされます
ちなみに、”gradients”: trueで背景にグラデーションもつかえます
attributesの「textColorやbackgroundColor」はデフォルト値を設定するときだけ追加します
(注意)カラーパレットに色を登録したときはCSSが必要です

{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 2,
    "name": "create-block/test",
    "version": "0.1.0",
    "title": "Test",
    "category": "design",
    "icon": "smiley",
    "description": "test",
    "attributes": {
        "content": {
            "type": "string",
            "source": "html",
            "selector": "p"
        },
        "alignment": {
            "type": "string"
        },
        "shadow":{
            "type": "boolean",
            "default": false
        },
        "shadowOpactiy": {
            "type": "number",
            "default": 0.3
        },
        "backgroundColor": {
            "type": "string",
            "default": "light-magenta"
        }
    },
    "supports": {
        "html": true,
        "color": {
            "background": true,
            "text": true,
            "gradients": true
        }
    },
    "textdomain": "test",
    "editorScript": "file:./build/index.js",
    "editorStyle": "file:./build/index.css",
    "style": "file:./build/style-index.css"
}

「index.jsファイル」はなにもしません
「PanelColorSettings(カラーパレット)」「style属性・class属性」も不要です
「editでprops」を受け取るだけです

import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
import {
    useBlockProps,
    RichText,
    AlignmentToolbar,
    BlockControls,
    InspectorControls,
} from '@wordpress/block-editor';
import classnames from "classnames";
import { RangeControl, PanelBody } from "@wordpress/components";
import './style.scss';
import './editor.scss';

registerBlockType('create-block/test', {
edit(props) {
    const { attributes, setAttributes } = props;
    const { alignment, content, shadow, shadowOpacity} = attributes;
    const onChangeAlignment = ( newAlignment ) => {
        setAttributes( {
        alignment: newAlignment === undefined ? 'none' : newAlignment,
        } );
    };
    const blockProps =  useBlockProps( {
        className: classnames( {
            "has-shadow": shadow,
            [`shadow-opacity-${shadowOpacity * 100}`]: shadowOpacity,
            [`text-align-${alignment}`]: alignment,
        }),
    });
 return (
    <>
    <InspectorControls>
        <PanelBody title={"Setting"}>
        {shadow && (
            <RangeControl
                label={"Shadow Opacity"}
                value={shadowOpacity}
                onChange={( shadowOpacity ) => setAttributes( {shadowOpacity } )}
                min={0.1}
                max={0.4}
                step={0.1}
            />
        )}
        </PanelBody>
    </InspectorControls>
    <BlockControls
        controls={[
        {
            icon: "chart-bar",
            title: "Shadow",
            onClick: () => {setAttributes({shadow: !shadow})},
            isActive: shadow
        }
        ]}
        >
        <AlignmentToolbar
            value={ alignment }
            onChange={ onChangeAlignment }
        />
    </BlockControls>
    <RichText  {...blockProps}
        allowedFormats={ [ 'core/bold', 'core/italic', 'core/link' ] }
        tagName="p"
        onChange={ ( content ) => setAttributes( { content } ) }
        value={content}
    />
    </>
    );
    },

    save({ attributes }) {
    const { alignment, content, shadow, shadowOpacity } = attributes;
    const blockProps =  useBlockProps.save( {
        className: classnames( {
            "has-shadow": shadow,
            [`shadow-opacity-${shadowOpacity * 100}`]: shadowOpacity,
            [`text-align-${alignment}`]: alignment,
        }),
    });

return (
    <>
    <RichText.Content { ...blockProps }
        tagName="p"
        value={content}
    />
    </>
    );
    },

} );

「alignment」も「text-align-[alignment]」のclass属性対応に変更したので、CSSを追加しています

.wp-block-create-block-test {
    padding: 1rem;
  &.has-shadow{
    box-shadow: 5px 5px 10px rgba(0,0,0,0.2);
    &.shadow-opacity-10 {
        box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.1);
        }
    &.shadow-opacity-20 {
        box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2);
        }
    &.shadow-opacity-30 {
        box-shadow:5px 5px 10px rgba(0, 0, 0, 0.3);
        }
     &.shadow-opacity-40 {
        box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.4);
        }
    }
    &.text-align-right {
        text-align: right;
    }
    &.text-align-center {
        text-align: center;
    }
    &.text-align-left {
        text-align: left;
    }
}
//カラーパレットに登録した色(仮)
.has-strong-magenta-background-color {
    background-color: #a156b4;
}
.has-strong-magenta-color {
    color: #a156b4;
}
.has-light-magenta-background-color {
    background-color: #d0a5db;
}
.has-light-magenta-color {
    color: #d0a5db;
}

ブロックテーマで利用するブロックは、ブロック設定サイドバーに表示されるユーザー向けコントロールのサポート(supportsプロパティ)を登録する必要があります
*Theme.jsonでサポートを登録していないブロックに新しいコントロールを追加することはできません(無効にすることはできます)

非推奨のバージョン追加について

なんらかの理由で、ブロックの属性を変更するときの対応です

選択肢は2つです

  • 既存のブロックはそのままにして、異なる名前で新しいブロックを作成する
  • ブロックの「deprecated (非推奨)」バージョンを提供する

複数の非推奨プロセスを管理することを加味して、非推奨プロセスを定数に保存し、これらをdeprecated配列に追加します
配列内は時系列の逆順 (新しいものが先) で格納します

const v1 = {};
const v2 = {};
const v3 = {};
const deprecated = [ v3, v2, v1 ];

「attributes・supports・save・migrate 」を非推奨オブジェクトで定義します

以下を想定した「deprecated」の例です

  • 「selector」 を「pからh3」に変更
  • 「alignment」を「textaling」に変更

「deprecated」バージョンを提供しない場合、ブロックを使用している箇所では右のようになります

別ファイル「v1.js」を作成しました

import { useBlockProps, RichText } from '@wordpress/block-editor';
import classnames from 'classnames';
import { omit } from 'lodash';
//block.jsonをインポート
import blockData from '../block.json';

const v1 = {
//block.jsonのsupportsをオブジェクトに
    supports: {
        html: true,
        color: {
            background: true,
            text: true,
            gradients: true
        }
    },
//変更前のattributes( ...blockData.attributesで展開)
//lodashのomit(オブジェクト、除外するプロパティキー)
    attributes: {
        ...omit( blockData.attributes, [ 'textalign' ] ),
        alignment: {
            type: 'string',
            default: 'left',
        },
        content: {
            type: 'string',
            source: 'html',
            selector: 'p',
        },
    },
//migrate メソッドを含む非推奨プロセスはスキップされて、saveが実行されます
//editで、古いバージョンを実行するため、古いバージョンのattributesに新いバージョンのattributesを含みます
    migrate: ( attributes ) => {
        return {
            //古いバージョンalignmentは削除して
            ...omit( attributes, [ 'alignment' ] ),
            //新いバージョンのキーに値を含めます
            textalign: attributes.alignment,
        };
    },
//変更前のsave
  save({ attributes }) {
    const { alignment, content, shadow, shadowOpacity } = attributes;
    const blockProps =  useBlockProps.save( {
        className: classnames( {
            "has-shadow": shadow,
            [`shadow-opacity-${shadowOpacity * 100}`]: shadowOpacity,
            [`text-align-${alignment}`]: alignment,
        }),
    });
return (
    <>
    <RichText.Content { ...blockProps }
        tagName="p"
        value={content}
    />
    </>
    );
    },
};

export default v1;
//省略
//v1をインポート
import v1 from './v1';

registerBlockType('create-block/test', {
edit(){},//中は省略
save(){},//中は省略

//deprecated追加
   deprecated: [ v1 ],
} );

カスタムブロックパターン

ブロックパターンは、事前定義されたカスタマイズ可能なブロックまたはブロックのグループです
簡易的なテンプレートとして利用することができます (変更は元のブロックパターンや他のコンテンツには影響しません)
*ちなみに再利用可能ブロックは使用されているすべての場所で変更内容が反映されます

パターンは「PHPファイル1枚だけ」でお手軽に登録できます
パターンディレクトリからコピーして利用できます

カスタムパターン

今までは「register_block_type」でブロックタイプを登録しました
カスタムブロックパターンは、「register_block_pattern 」を使用して登録します

「register_block_pattern」は「add_action」の「init フック」から呼びます

「register_block_pattern 」の引数は2つ
title: 機械用のタイトル(命名規約は namespace/title)
properties: パターンのプロパティを説明する配列(プロパティのtitleとcontentは必須)

<?php
/**
 * Plugin Name: pattern

 */
//独自のブロックパターンカテゴリーを登録する
function hoge_cat() {
    register_block_pattern_category( 'block-pattern', array(
        'label' => 'my-pattern'
    ));
}
add_action( 'init', 'hoge_cat' );

//カスタムブロックパターンを登録
function hoge() {
    register_block_pattern( 'block-pattern/my-patterns', array(
        'title' => 'My Pattern',
        'description' =>'',
        'categories' => array('my-pattern'),
        'keywords' => array('my pattern'),
//contentにはパターンのブロックのHTMLマークアップ(コードエディタで表示)を貼りつけます
        'content' => ''
    ));
}
add_action( 'init', 'hoge' );
コードエディターを選択

コードエディターでHTMLマークアップを表示

JSON Escapeを使いでマークアップを左側のパネルに貼り付け「Escape」を選択して、出力コードをコピーします

コードを貼り付ける

contentの値に貼りつけます

書式設定ツールバーにカスタムボタンを設定

ツールバーにボタンを追加して、選択したテキストにフォーマットを適用します

テキストフォーマット
smallボタンを追加

@wordpress/scriptsを利用する

「register_block_type」は利用しないので、@wordpress/scriptsを利用して環境を作ります

wp-content/plugins/の直下にプロジェクトのフォルダを作成

npm init -y
npm install @wordpress/scripts --save-dev

package.jsonファイルの「main」を変更し、「scripts」に「buildとstart」を追加します

"main": "build/index.js",

"scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
},

プロジェクトのフォルダ直下に「plugin.phpファイル」を作ります

<?php
/**
 * Plugin Name:       format-type
 *
 */

function my_theme_plugins_format_enqueue_assets() {

    $asset_file = include(plugin_dir_path( __FILE__ ) . 'build/index.asset.php');

    wp_enqueue_script( 'my-theme-plugins-format-script',
     plugins_url('build/index.js', __FILE__),
     $asset_file['dependencies'], $asset_file['version']);
//cssが必要な時
  //  wp_enqueue_style( 'my-theme-plugins-format-style',
  //  plugins_url('build/index.css', __FILE__) );
}

add_action( 'enqueue_block_editor_assets', 'my_theme_plugins_format_enqueue_assets' );

「プラグインヘッダ」と「JavaScriptをロードするコード」を追加します
「JavaScriptをロードするコード」は「wp_enqueue_script()」でキューに入れます(WordPress側で挿入する)
「wp_enqueue_script()」の基本的なパラメーター(ハンドル名・スクリプトファイルへの完全なURL・依存しているスクリプトタグの配列)

アクションフックのいずれかからキューに入れる必要があります
例えば「enqueue_block_editor_assets」はブロックエディターのロード時に呼び出されるフックです

@wordpress/scriptsでビルドすると、依存関係の配列とバージョン番号を含むファイル「index.asset.php」が生成されます
下記のように書くことで、依存関係は手動で更新する必要はありません

$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php');
 
wp_enqueue_script(
    'ハンドル名',
    plugins_url( 'build/index.js', __FILE__ ),
    $asset_file['dependencies'],
    $asset_file['version']
);

プロジェクトのフォルダ直下に「src」フォルダを作り、そこに「index.js(エンドポイント)」を作成します
*必要に応じて「editor.scss」を作成
使用可能なフォーマットタイプのリストは「core/rich-text」で管理されています
「RichTextToolbarButton」を利用してツールバーにボタンを追加します

import { registerFormatType, toggleFormat } from '@wordpress/rich-text';
import { RichTextToolbarButton } from '@wordpress/block-editor';
//import './editor.scss'

registerFormatType('my-theme/small', {
    title:'Small',
    tagName: 'small',
    className: null,
    edit({ isActive, value, onChange }){
        return (
            <RichTextToolbarButton
                icon="edit"
                title='Small'
                onClick={() => {
                    onChange(
                        toggleFormat(value, {
                            type: 'my-theme/small',
                            attributes: {
                                style: 'font-size: 0.8em',
                            },
                        })
                    );
                }}
                isActive={isActive}
            />
        );
    }
});

*上の例(style: ‘font-size: 0.8em’)のように、スタイルタグにハードコードすると、プラグインを無効にしてもスタイルは残ります

Block Filters

既存のブロックの動作を変更フィルターについて

JavaScript側で、既存のブロックの「metadataを変更」「class名を変更」の例
*(注意)そのブロックで既に作成した投稿は壊れます

import { addFilter } from '@wordpress/hooks';
//supportsのclassNameをtrueに変更
const changeSupport = (settings, blockName) => {
	if (blockName === '既存のブロック登録名') {
        return { ...settings, supports: {className: true}};
	}
	return settings;
};
addFilter(
	'blocks.registerBlockType',
	'ネームスペース/新しい名前',
	changeSupport
);

//class名を変更する
const changeClass = (className, blockName) => {
     return blockName === '既存のブロック登録名' ? '変更後のclass' : className;
};
addFilter(
	'blocks.getBlockDefaultClassName',
	'ネームスペース/新しい名前',
	changeClass
);
,