WordPressブロックエディタのツールバーをカスタマイズする方法

ブロックエディタのカスタマイズをしていますが、ブロックエディタのカスタマイズ方法に関する情報がネット上にはまだ少なく、検索しては開発環境でコードを書き、ビルド後に検証するというのを繰り返してします。

そんな中で、今回は、各ブロックのツールバーに、特定の文字列をマークアップするためのボタンを追加するカスタマイズ方法をご紹介します。

前提として、開発環境は、Node.js@wordpress/scriptsを使用し、ESNextでコードを記述しています。

ツールバーに要素挿入ボタンを追加する

ブロックエディタのツールバーには、デフォルトで「太字 (strong要素)」「イタリック (em要素)」「リンク (a要素)」「インラインコード (code要素)」「インライン画像 (img要素)」「打ち消し (s要素)」を挿入するためのボタンがあります。

それらに、属性の無いsmall要素のボタンを追加する場合は、次のようなコードになります。

/* 1 */
import { RichTextToolbarButton } from '@wordpress/editor';
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { registerFormatType, toggleFormat } from '@wordpress/rich-text';

/* 2 */
registerFormatType( 'my-editor/small', {

  /* 3 */
  title: __( 'Small print', 'my-editor' ),

  /* 4 */
  tagName: 'small',

  /* 5 */
  className: null,

  /* 6 */
  attributes: {},

  /* 7 */
  edit( { isActive, value, onChange } ) {
    return createElement( RichTextToolbarButton, {

      /* 8 */
      title: <small>{ __( 'Small print', 'my-editor' ) }</small>,

      /* 9 */
      icon: 'edit',

      /* 10 */
      isActive: isActive,

      /* 11 */
      onClick: () => onChange( toggleFormat( value, {
        type: 'my-editor/small',
        attributes: {}
      } ) )

    } );
  }

} );
1. import { コンポーネント } from パッケージ; の部分
使用するコンポーネントをインポートします。パッケージとコンポーネントを正しく設定しないと正常にビルドできません。どのようなパッケージとコンポーネントがあるかは、Package Reference | Block Editor Handbook | WordPress Developer Resourcesで確認できます。
2. registerFormatType() の部分
要素を登録します。
3. title の部分
ボタンの名称です。
4. tagName の部分
登録する要素名です。
5. className の部分
登録する要素に設定するclass属性値です。必要無い場合は null を設定します。
6. attributes の部分
登録する要素に設定する属性です。className 以外のclass属性値や、その他の属性を設定します。
7. edit() の部分
表示されるボタンを設定します。
8. title の部分
ボタンの名称です。ドロップダウン時に表示されます。JSXで記述すると、そのまま反映されます。
9.icon の部分
ボタンに表示されるアイコンを設定します。DashiconsやSVGコードが設定できます。Dashiconsを使用する場合は「dashicons-XXXX」の「XXXX」の部分を設定します。
10. isActive の部分
選択された状態かどうかを判別する条件を設定します。
11. onClick の部分
選択された時に toggleFormat() により要素の挿入・除去を行います。

要素によっては、title属性が特別な意味を持ったり、専用の属性がある場合もあります。例えば、略称を意味するabbr要素のtitle属性には省略されていない名称を指定します。

abbr要素の使用例: <abbr title="HyperText Markup Language">HTML</abbr>

このような要素をツールバーに追加する場合は、次のようなコードになります。

import { RichTextToolbarButton } from '@wordpress/editor';
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { registerFormatType, toggleFormat } from '@wordpress/rich-text';

registerFormatType( 'my-editor/small', {
  …
} );

registerFormatType( 'my-editor/abbr', {
  title: __( 'Abbreviation', 'my-editor' ),
  tagName: 'abbr',
  className: null,

  /* 1 */
  attributes: {
    title: ''
  },

  edit( { isActive, value, onChange } ) {
    return createElement( RichTextToolbarButton, {
      title: <abbr>{ __( 'Abbreviation', 'my-editor' ) }</abbr>,
      icon: 'edit',
      isActive: isActive,
      onClick: () => {

        /* 2 */
        let attributes = {};
        let attrTitle = !isActive ? window.prompt( __( 'Enter title attribute (required)', 'my-editor' ) ) : '';
        if( attrTitle ) {
          attributes.title = attrTitle;
        }
        onChange( toggleFormat( value, {
          type: 'my-editor/abbr',
          attributes: attributes
        } ) );

      }
    } );
  }
} );
1. attributes の部分
登録する要素に設定する属性とデフォルトの属性値を設定します。
2. let attributes の部分
ボタンが押された時に入力欄を表示し、入力されたら attributesattributes.title を追加します。そして、toggleFormat()attributes の値として設定します。

これで、title属性が設定できるabbr要素のボタンが追加されます。

ツールバーに1つだけ選択できるドロップダウンメニューを追加する

「alignleft」「alignright」「aligncenter」「alignwide」「alignfull」を選択するツールバーボタンのようなドロップダウンメニューが追加できます。

次のコードは、選択された文字列を <span class="has-text-color has-XXXX-color"> … </span> でマークアップして文字色を設定できるようにします。

import { Toolbar } from '@wordpress/components';
import { BlockFormatControls } from '@wordpress/editor';
import { __ } from '@wordpress/i18n';
import { applyFormat, getActiveFormat, registerFormatType, removeFormat } from '@wordpress/rich-text';

registerFormatType( 'my-editor/color', {
  title: __( 'Text color', 'my-editor' ),
  tagName: 'span',
  className: 'has-text-color',

  /* 1 */
  attributes: {
    class: ''
  },

  edit( { isActive, value, onChange } ) {

    /* 2 */
    const controls = [
      {
        title: __( 'Red', 'my-editor' ),
        class: 'has-red-color'
      },
      {
        title: __( 'Green', 'my-editor' ),
        class: 'has-green-color'
      },
      {
        title: __( 'Blue', 'my-editor' ),
        class: 'has-blue-color'
      }
    ];

    /* 3 */
    let activeClass;
    if( isActive ) {
      const activeFormat = getActiveFormat( value, 'my-editor/color' );
      activeClass = activeFormat.attributes.class;
    }

    /* 4 */
    return (
      <BlockFormatControls>
        <Toolbar icon='admin-customizer' label={ __( 'Text color', 'my-editor' ) } isCollapsed={ true } popoverProps={ { position: 'bottom right' } } controls={ controls.map( ( control ) => {
          return {
            icon: 'admin-customizer',
            title: <span class={ control.class }>{ control.title }</span>,
            isActive: activeClass === control.class,
            onClick: () => {
              if( activeClass === control.class ) {
                onChange( removeFormat( value, 'my-editor/color' ) );
              } else {
                onChange( applyFormat( value, {
                  type: 'my-editor/color',
                  attributes: {
                    class: control.class
                  }
                } ) );
              }
            }
          };
        } ) } />
      </BlockFormatControls>
    );

  }
} );
1. attributes の部分
挿入する要素に className 以外のclass属性値を設定できるようにします。
2. const controls の部分
適用できる文字色の名称とclass属性値の一覧を設定します。
3. let activeClass の部分
選択された文字列に文字色が適用されていたら、その文字色のclass属性値を取得します。
4. return の部分
<BlockFormatControls><Toolbar … /></BlockFormatControls> でドロップダウンメニューが作られます。「Toolbar」の「controls」に const controls で設定した文字色を項目として追加します。let activeClass と同じclass属性値が設定された項目が選択されたら removeFormat() で要素を除去し、そうでなければ applyFormat() で要素を挿入します。

ツールバーにボタンをグループ化したドロップダウンメニューを追加する

要素挿入ボタンを独自にグループ化したドロップダウンメニューが追加できます。

import { DropdownMenu, Fill, Slot, Toolbar, ToolbarButton } from '@wordpress/components';
import { BlockFormatControls } from '@wordpress/editor';
import { Fragment } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { displayShortcut } from '@wordpress/keycodes';
import { registerFormatType, toggleFormat } from '@wordpress/rich-text';
const { orderBy } = lodash;

/* 1 */
const formats = [
  {
    name: 'my-editor/text-style-italic',
    title: __( 'Italic', 'my-editor' ),
    tag: 'span',
    icon: 'editor-italic',
    class: 'is-style-italic'
  },
  {
    name: 'my-editor/text-style-bold',
    title: __( 'Bold', 'my-editor' ),
    tag: 'span',
    icon: 'editor-bold',
    class: 'is-style-bold'
  },
  {
    name: 'my-editor/text-style-underline',
    title: __( 'Underline', 'my-editor' ),
    tag: 'span',
    icon: 'editor-underline',
    class: 'is-style-underline'
  }
];

registerFormatType( 'my-editor/text-style', {
  title: __( 'Text style', 'my-editor' ),
  tagName: 'span',
  className: 'text-style',
  edit( { isActive, value, onChange } ) {

    /* 2 */
    return (
      <BlockFormatControls>
        <Toolbar>
          <Slot name='MyEditorTextStyleToolbarControls'>{ ( fills ) => {
            return ( fills.length !== 0 && <DropdownMenu icon='edit' label={ __( 'Text style', 'my-editor' ) } position='bottom left' hasArrowIndicator={ true } controls={ orderBy( fills.map( ( [ { props } ] ) => props ), 'title' ) } /> );
          } }</Slot>
        </Toolbar>
      </BlockFormatControls>
    );

  }
} );

formats.map( ( format ) => {
  registerFormatType( format.name, {
    title: format.title,
    tagName: format.tag,
    className: format.class,
    edit( { isActive, value, onChange } ) {

      /* 3 */
      const MyEditorTextStyleToolbarButton = function( { name, shortcutType, shortcutCharacter, ...props } ) {
        let fillName = 'MyEditorTextStyleToolbarControls';
        let shortcut;
        if( name ) {
          fillName += `.${ name }`;
        }
        if( shortcutType && shortcutCharacter ) {
          shortcut = displayShortcut[ shortcutType ]( shortcutCharacter );
        }
        return (
          <Fill name={ fillName }>
            <ToolbarButton { ...props }>{ () => { return { shrotcut: shortcut } } }</ToolbarButton>
          </Fill>
        );
      };

      const Tag = format.tag;
      const title = <Tag class={ format.class }>{ format.title }</Tag>;
      const onClick = () => onChange( toggleFormat( value, {
        type: format.name
      } ) );
      return (
        <Fragment>
          <MyEditorTextStyleToolbarButton title={ title } icon={ format.icon } isActive={ isActive } onClick={ onClick } />
        </Fragment>
      );
    }
  } );
} );
1. const formats の部分
挿入する要素の一覧を作成します。
2. return の部分
ツールバーに表示するドロップダウンメニューのボタンを生成します。ドロップダウンメニューの項目が追加できるよう「MyEditorTextStyleToolbarControls」という「Slot」を用意します。
3. const MyEditorTextStyleToolbarButton の部分
Fill」を設定した「MyEditorTextStyleToolbarButton」という独自の「ToolbarButton」を作成します。この「ToolbarButton」を使って「MyEditorTextStyleToolbarControls」に項目を追加します。

本当に実現したいツールバーボタンはこんなんじゃない

今回、紹介したコードを利用すれば、クラシックエディタでできて、ブロックエディタでできなかった、特定の文字列にスタイルを適用するということが実現できます。が、不十分でもあります。

これらのボタンは常に、tagName (要素) とclassName (class属性) がセットになっています。

例えば「ツールバーに要素挿入ボタンをグループ化したドロップダウンメニューを追加する」で作られたボタンを使って、「Italic (斜体)」「Bold (太字)」「Underline (下線)」のすべてを文字列に設定した場合、そのHTMLコードは次のようになります。

<span class="is-style-italic"><span class="is-style-bold"><span class="is-style-underline"> … </span></span></span>

本当に実現したいツールバーボタンは、要素を挿入・除去するボタンと、class属性値を追加・削除するボタンの2種類です。

class属性値を追加・削除するボタンは、選択した文字列に要素が挿入済みであれば、その要素のclass属性値を追加・削除する。そうでなければ、class属性値が設定されたspan要素を挿入・除去する。みたいなことが簡単にできるようになるといいんですけどね。そうすれば、

<em class="is-style-italic is-style-bold is-style-underline"> … </em>

のように、span要素以外の要素にclass属性値をまとめて設定するといったことができるんですけど…。

ということで、「『本当に実現したいツールバーボタン』をうちではもう実装してるよ。」とか、「こうするといいよ。」など、ブロックエディタに精通している方は、コメントなどで情報提供お願いします

関連記事