WordPressのブロック開発メモ その6-1 スロットとプラグイン

前回ではスロットを紹介しました。

WordPressのブロック開発メモ その6 スロット

今回はスロットを裏で支えるプラグインについても掘り下げていこうと思います。

プラグインに追加すること前提で提供されているコンポーネントたちです。

2023/02/26時点。

  • MainDashboardButton
  • PluginBlockSettingsMenuItem
  • PluginDocumentSettingPanel
  • PluginMoreMenuItem
  • PluginPostPublishPanel
  • PluginPostStatusInfo
  • PluginPrePublishPanel
  • PluginSidebar
  • PluginSidebarMoreMenuItem

この中からPluginBlockSettingsMenuItem, PluginDocumentSettingPanel, PluginMoreMenuItemを抜粋して試してみましょう。

このうち多くのコンポーネントは@wordpress/edit-postにあります。
またプラグインは@wordpress/pluginsにあります。

なので先に型定義をインストールします。

npm install --save-dev @types/wordpress__edit-post
npm install --save-dev @types/wordpress__plugins

スロットを表示。

これらのコンポーネントはプラグインに追加する前提なのですが、まだプラグインを説明してないのでバッドな方法ではありますがブロックに直接書いています。
参考程度で決して実戦で使用しないでください。
なぜプラグインが必要なのかを理解する前座のようなものだと思ってください。

Bad Sample Code!

import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { PluginBlockSettingsMenuItem, PluginDocumentSettingPanel, PluginMoreMenuItem } from '@wordpress/edit-post';
import { wordpress, alignNone, addCard } from '@wordpress/icons';

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

            <h2>スロットスロットランランンラン</h2>

            <PluginBlockSettingsMenuItem
                icon={ wordpress }
                label="ツールバーのメニューに表示されまっす!"
                onClick={e => {}} />

            <PluginDocumentSettingPanel
                icon={ alignNone }
                title="投稿サイドバー!"
                >
                右側の投稿サイドバーに表示されまっす!
            </PluginDocumentSettingPanel>

            <PluginMoreMenuItem
                icon={ addCard}
                onClick={() => alert('その他メニュー!')}
                >
                右上に表示されているメニューでっす!
            </PluginMoreMenuItem>

        </div>
    );
}

PluginBlockSettingsMenuItem

ツールバーのメニューに追加される

PluginDocumentSettingPanel

編集ページの右上のメニューに表示される

PluginMoreMenuItem

ブロックを選択するとブロックサイドバーが現れます。
ブロック以外の場所をクリックしてブロックのフォーカスを外すと投稿サイドバーが表示されます。
そこに追加されます。

なぜいけない?

何故バッドなコードなのかというと前回説明した通り、ブロックを複数追加すると複数のFillが発生してしまうことになります。
どうなるか実験してみましょう。

同じブロックを3つに追加します。

とりあえずそのうちのPluginDocumentSettingPanelに注目してみます。
ブロックと同じく三つ分追加されてしまいます。
他のスロットも同様の結果になります。

そこでプラグインの出番です。

プラグインに登録

そこでプラグインが登場します。

import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { PluginBlockSettingsMenuItem, PluginDocumentSettingPanel, PluginMoreMenuItem } from '@wordpress/edit-post';
import { wordpress, alignNone, addCard } from '@wordpress/icons';
import { registerPlugin } from '@wordpress/plugins';

const PluginBlockSettingsMenuItemRender = () =>
{
    return (
        <PluginBlockSettingsMenuItem
            allowedBlocks={ ['create-block/kurage-worker'] }
            icon={ wordpress }
            label="ツールバーのメニューに表示されまっす!"
            onClick={e => { alert('うなぎたべたい・・・') }} />
    )
}

const PluginDocumentSettingPanelRender = () =>
{
    return (
        <PluginDocumentSettingPanel
            icon={ alignNone }
            title="投稿サイドバー!"
            >
            右側の投稿サイドバーに表示されまっす!!
        </PluginDocumentSettingPanel>
    )
}

const PluginMoreMenuItemRender = () =>
{
    return (
        <PluginMoreMenuItem
            icon={ addCard}
            onClick={() => alert('かつ丼たべたい・・・')}
            >
            右上に表示されているメニューでっす!
        </PluginMoreMenuItem>
    )
}

// @ts-expect-error
registerPlugin('kurage-slot-slot-1', {
    render: PluginBlockSettingsMenuItemRender
});

// @ts-expect-error
registerPlugin('kurage-slot-slot-2', {
    render: PluginDocumentSettingPanelRender
});

// @ts-expect-error
registerPlugin('kurage-slot-slot-3', {
    render: PluginMoreMenuItemRender
});

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

            <h2>スロットスロットランランンラン</h2>

        </div>
    );
}

どうも現時点で型定義が信用出来ないのでエラーは握りつぶしてます。
エラーを握りつぶすためコメントで@ts-expect-errorを指定してます。

registerPlugin()でプラグインを登録します。
一つ目の引数はプラグイン名で重複は避けてください。
二つ目の引数でicon, render, scopeなどを設定しますが今回はrenderだけ見ていきます。
このrenderにコンポーネントを渡すことで前回の問題を回避できます。

allowedBlocks={ ['create-block/kurage-worker'] }

PluginBlockSettingsMenuItemallowedBlocksはどのブロックに追加するかです。
今回は自分の作ったブロック(create-block/kurage-worker)のツールバーにだけ追加するという意味です。
これを指定しないと他人の作ったブロックや元から用意されているブロックのツールバーにまで追加されてしまいます。

どこに配置された?

このrenderで指定したコンポーネントはある場所(プラグインエリア)に追加されます。

一応それらしきものはあるっぽい。

さて、プラグインの使い方は分かりました。

でもプラグインの正体がよくが分かりません。

ただ事前に言っておきますが、

多分ブロック開発者にとってプラグインは不要な知識だと思います。

ここから先はちょっと無駄な知識になる可能性があります。

プラグイン実装

プラグインは@wordpress/pluginsパッケージで実装されています。
非常に単純なパッケージなのでコードを読んだ方が早いかもしれません。

詳細を一切考えず大体の構造を頭の中でイメージすると

プラグインを登録するregisterPlugin()、プラグインを取得するgetPlugins()getPlugin()があり、現在登録されているプラグインをレンダリングするコンポーネントPluginAreaが存在します。

めっちゃ適当ですが図解で見てみましょう!

以下はスロットをプラグインに登録し、そのプラグインを取得する例です。

import React from 'react';
import KurageExampleBlockProps from './props';
import './editor.scss';
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { wordpress, alignNone, addCard, download, external } from '@wordpress/icons';
import { getPlugin, getPlugins, registerPlugin } from '@wordpress/plugins';

registerPlugin('kurage-slot-slot-1', {
    // @ts-ignore
    icon: wordpress,
    render: () => <p>Slot 1</p>,
    scope: 's-b'
});

registerPlugin('kurage-slot-slot-2', {
    // @ts-ignore
    icon: alignNone,
    render: () => <p>Slot 2</p>,
    scope: 's-a'
});

registerPlugin('kurage-slot-slot-3', {
    // @ts-ignore
    icon: addCard,
    render: () => <p>Slot 3</p>,
    scope: 's-a'
});

registerPlugin('kurage-slot-slot-4', {
    // @ts-ignore
    icon: download,
    render: () => <p>Slot 4</p>,
    scope: 's-b'
});

registerPlugin('kurage-slot-slot-5', {
    // @ts-ignore
    icon: external,
    render: () => <p>Slot 5</p>,
    scope: 's-c'
});

console.log("getPlugins")

// @ts-ignore
const plugins = getPlugins('s-b');
console.log(plugins);

console.log("getPlugin")
const plugin = getPlugin('kurage-slot-slot-3');
console.log(plugin);

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

            <h2 className="xx">スロットスロットランランンラン</h2>

        </div>
    );
}

registerPlugin()のオプションでscopeを指定すると、プラグインはそのスコープに属します。

getPlugin()getPlugins()も追加したプラグインを取得します。

getPlugins()はそのスコープ(scopeで指定した名前)に属するプラグインを複数取得します。
getPlugin()はプラグイン名(一意である必要あり)から一つ取得します。

getPlugins()s-bを渡すと、プラグインのscopes-bを指定したkurage-slot-slot-1kurage-slot-slot-4が取得されます。

またプラグインのスコープは無名であることが出来るようです。
無名にするとgetPlugins()scopeを指定してないプラグインを取得します。

現在プラグインは内部的にグローバルなリテラルオブジェクトとして保持されているもようです。
プラグイン名がそのプロパティ名になるので重複するとおかしなことになります。

次にプラグインのレンダリングです。
プラグインに設定されたrenderをレンダリングする場所はPluginAreaです。

import { PluginArea } from '@wordpress/plugins';

const PluginArea2 = PluginArea as any;

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

            <h2>スロットスロットランランンラン</h2>

            <p>スコープ s-a</p>
            <PluginArea2 scope="s-a" />

            <p>スコープ s-b</p>
            <PluginArea2 scope="s-b" />

        </div>
    );
}

ちょっと型定義がアレなのでPluginAreaanyに変換してます。

あれあれ?

表示されてませんね!

でもDOMを見てみるとちゃんと追加されています。

ただし、display: noneが設定されているため画面からは見えません。

無名のプラグインエリアはどこへ?

PluginAreascopeを省略出来ます。
無名のプラグインエリアになります。

既にワードプレス本体によってコンポーネント上位層に配置されてるはず。
テキストサーチしたところあった。

間違ったことを書いてるかもしれませんので眉唾程度で考えてください。
さすがにこれが正しいのかを調べるのは面倒なのでしません。

ブラウザではどう表示されているでしょう?

// @ts-ignore
registerPlugin('kurage-slot-slot-1', {

    render: () => <p className="marker">Slot 1</p>
});

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

            <h2>スロットスロットランランンラン</h2>

        </div>
    );
}

slotを指定せずにプラグインを追加。

DOMツリーをセレクタ(p.marker)でたどると見つかりました。
一応Chromeの開発者ツールからCSSのdisplay: noneは削除しました。

ちょっと話はそれますが、よく見ると上に.popover-slotを持ったdivがいますね。
本家ドキュメントにpopovertooltipの解説にスロットを参照するよう書かれてましたが、全然詳しいこと書かれて無かった。
仕方なくソースコードを読んでいるうちにここまでたどり着いたんですね(苦労した!)。

なぜ?

なぜプラグインエリアはnoneなのか?

多分、多分だけど、プラグイン自体はただのFillの配置場所としてあるんではないかなと。
一旦Fillを非表示のプラグインエリアに配置して、Scopeが存在したらそっちにDOMを表示すると。
ただの場所提供みたいな。

あとFillについてですが、ソースコードを眺めるとレンダリングにはcreatePortal()が使われています。
第二引数がスロットのノードになっており、対象のスロットが存在しなかったらそもそもレンダリングされません。

つまり、

  • 無名のプラグインエリアを配置
  • プラグインを登録する
  • プラグインエリアに配置されるが非表示
  • もしプラグインがFillの場合、対象のスロットが無ければレンダリングされない
  • ただ対象のスロットがあればそちらに表示される

もう文書書く能力無さ過ぎて何言ってるのかわからないと思いますが何となくのイメージです。
これがスロットとプラグインの合わせ技みたいな。

間違ってたら本当にごめんなさい、先に誤っておきます。

スロットとの融合

プラグインの仕組みは把握した。
後はスロットが存在しない時のFillはレンダリングされないことを確かめてみます。
複製されてしまうブロック内にスロット配置するのはどうかとは思いますけど実験してみましょう。

スロットがある場合とない場合でFillがどうなるのか?

対応するスロットが存在する場合

registerPlugin('kurage-slot-slot-1', {

    render: () => <MyCustomBlock><h2 className="marker">うなぎぱい!</h2></MyCustomBlock>
});

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

            <h2>スロットスロットランランンラン</h2>

            <div style={{borderWidth: 1, borderColor: 'red', borderStyle: 'solid', padding: 5}}>
                <MyCustomBlock.Slot />
            </div>

        </div>
    );
}

対応するスロットがあるのでそちらに移転してます。
.markerをもったh2が確認出来ます。

対応するスロットが存在しない場合

// @ts-ignore
registerPlugin('kurage-slot-slot-1', {

    render: () => <MyCustomBlock><h2 className="marker">うなぎぱい!</h2></MyCustomBlock>
});

export default (props: BlockEditProps<KurageExampleBlockProps>) =>
{
    return (
        <div {...useBlockProps()}>

            <h2>スロットスロットランランンラン</h2>

            <div style={{borderWidth: 1, borderColor: 'red', borderStyle: 'solid', padding: 5}}>
                Remove MyCustomBlock.Slot
            </div>

        </div>
    );
}

コンポーネントは無名プラグインエリアにあるものの、
DOM検索で.markerは見つからずDOMは生成されてないっぽい。

次回は高階コンポーネントについてです。

WordPressのブロック開発メモ その7 HOC(高階コンポーネント)

certificate Docker Gutenberg Hyper-V openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク