ブロックエディターで段落を選ぶと、ツールバー左端のアイコンから「見出し」「リスト」「引用」などに作り替えられます。ところが自作したカスタムブロックは、この変換の候補にも変換先にも出てきません。候補として並ぶかどうかは、ブロック自身が transforms で「どのブロックから来られるか」「どのブロックへ行けるか」を宣言しているかで決まります。この記事では、段落から自作ブロックへ変換できるようにする書き方を出発点に、from と to の違い、記号の入力や HTML の貼り付けをきっかけに変換する prefix / raw、古いショートコードを引き継ぐ shortcode までを解説します。WordPress 6 系、apiVersion 3 を前提としています。
目次
transforms はブロックの乗り換え先を宣言する設定
ブロックの変換は、エディターがブロックを削除して別のブロックを作り直す処理です。このとき元のブロックが持っていた内容を新しいブロックのどの属性に入れるかは、機械的には決められません。段落の文字列を見出しの content に渡すのが自然だとしても、その対応付けを知っているのはブロックを作った本人だけです。
そこで各ブロックは、受け入れられる変換元と、変換できる変換先を自分で列挙し、値の詰め替え方まで関数で書いておきます。それが transforms です。エディターは選択中のブロックと登録済みのすべてのブロックの transforms を突き合わせて、変換メニューに並べる候補を組み立てます。宣言していないブロックはどれとも繋がっていない孤立した状態なので、メニューには何も出ません。
transforms は block.json ではなく JavaScript 側に書く
ブロックの設定は block.json にまとめるのが基本ですが、transforms は例外で block.json には書けません。値の詰め替えを関数で書く必要があり、JSON では関数を表現できないためです。registerBlockType の第2引数、つまり edit や save と同じ場所に渡します。
block.json に transforms を書いてもエラーにはならず、単に無視されます。動かない原因として気づきにくいので覚えておいてください。
段落から自作ブロックへ変換できるようにする
例として、1行のメッセージを持つ「お知らせ」ブロックを使います。まずはブロック本体の定義です。message という属性にテキストを保存する、シンプルな静的ブロックです。
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "webool/notice",
"title": "お知らせ",
"category": "text",
"textdomain": "webool",
"attributes": {
"message": {
"type": "string",
"source": "html",
"selector": "p"
}
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css"
}
変換の定義は量が増えやすいので、別ファイルに切り出しておくと見通しが良くなります。段落から webool/notice へ変換する最小の from は次のように書きます。
import { createBlock } from '@wordpress/blocks';
const transforms = {
from: [
{
type: 'block',
// 変換元として受け入れるブロック(複数指定できる)
blocks: [ 'core/paragraph' ],
// attributes には変換元ブロックの属性が渡ってくる
transform: ( attributes ) =>
createBlock( 'webool/notice', {
message: attributes.content,
} ),
},
],
};
export default transforms;
あとはこれを登録時に渡すだけです。
import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import edit from './edit';
import save from './save';
import transforms from './transforms';
registerBlockType( metadata.name, {
edit,
save,
transforms, // block.json ではなくここで登録する
} );
ビルドしてエディターを開き、段落に文字を入れてからツールバー左端のアイコンをクリックすると、変換候補に「お知らせ」が並びます。選ぶと段落が消えて、入力していたテキストが message に入った状態のお知らせブロックに置き換わります。
createBlock で新しいブロックを組み立てる
transform の役目は、変換後のブロックオブジェクトを作って返すことです。自分で DOM を書き換えたり、エディターの状態を直接触ったりする必要はありません。@wordpress/blocks の createBlock にブロック名と属性を渡せば、それがそのまま新しいブロックとして挿入されます。
createBlock( name, attributes, innerBlocks );
// 例:内側にブロックを持たせる場合
createBlock( 'core/group', {}, [
createBlock( 'webool/notice', { message: 'ここに本文' } ),
] );
配列を返すこともできます。1つのブロックを複数のブロックに分解したいときは [ createBlock( … ), createBlock( … ) ] のように返せば、その並びで挿入されます。
to を書くと元のブロックに戻せる
from だけを書いた状態は「段落からお知らせへ」の片方向です。お知らせを選んでも変換メニューは空のままなので、間違えて変換したときに戻せません。逆方向は to に書きます。
const transforms = {
from: [
{
type: 'block',
blocks: [ 'core/paragraph' ],
transform: ( attributes ) =>
createBlock( 'webool/notice', {
message: attributes.content,
} ),
},
],
to: [
{
type: 'block',
// 変換先として選べるブロック
blocks: [ 'core/paragraph' ],
// attributes は自分(webool/notice)の属性
transform: ( attributes ) =>
createBlock( 'core/paragraph', {
content: attributes.message,
} ),
},
],
};
from と to で attributes の意味が入れ替わる点に注意してください。from では変換元のブロックの属性、to では自分自身の属性が渡ってきます。書いているファイルは同じなので、うっかり attributes.content と attributes.message を取り違えると、内容が空のブロックができあがります。
なお、同じ組み合わせの変換は from と to のどちらか一方に書けば動きます。段落からお知らせへの変換は、お知らせ側の from に書いても、段落側(コアブロック)を blocks.registerBlockType フィルターで拡張して to に足しても実現できます。自作ブロックのファイル内で完結する前者のほうが管理しやすいので、基本は自分のブロックに from と to を両方書く形で問題ありません。
変換のきっかけを決める type の種類
ここまで使ってきた type: 'block' は変換メニュー経由の変換です。transforms はほかにも、文字入力や貼り付けをきっかけにした変換を扱えます。type によって書ける方向が決まっており、block 以外は from 専用です。
| type | 方向 | 変換のきっかけ |
|---|---|---|
block | from / to | ツールバーの変換メニューから選ぶ |
prefix | from | 空の段落で決めた記号を打ち、スペースを入れる |
enter | from | 段落に決めた文字列を打って Enter を押す |
raw | from | HTML を貼り付ける |
shortcode | from | ショートコードを含む本文を貼り付ける・クラシックブロックを変換する |
files | from | ファイルをエディターにドラッグ&ドロップする |
すべてを用意する必要はありません。よく使うブロックほど入力の手間が気になるので、block に加えて prefix を足す、既存コンテンツからの移行が必要なら raw か shortcode を足す、といった順で考えると無駄がありません。
prefix:記号を打つだけでブロックを出す
見出しを作るときに # とスペース、リストを作るときに - とスペースを打つと、段落がその場でブロックに変わります。これは各ブロックが prefix を登録しているためです。自作ブロックにも同じ入り口を用意できます。
{
type: 'prefix',
// 空の段落で「!」+スペースを入力したときに発動する
prefix: '!',
// content には記号のあとに入力された文字が渡る
transform: ( content ) =>
createBlock( 'webool/notice', { message: content } ),
}
記号はコアブロックが使っているもの(#、-、>、1. など)と重ならないように選びます。重複した場合は後述の priority で決着しますが、書いた本人以外には予想しにくい挙動になるため避けたほうが無難です。
enter:入力した文字列そのものをきっかけにする
enter は、段落に入力した内容が正規表現に一致した状態で Enter を押したときに発動します。区切り線ブロックがハイフン3つで挿入できるのはこの仕組みです。
{
type: 'enter',
// 「!!!」だけを入力して Enter を押したとき
regExp: /^!{3}$/,
// 入力した文字は捨てて、空のお知らせブロックを作る
transform: () => createBlock( 'webool/notice' ),
}
prefix は入力途中のテキストを引き継げるのに対し、enter は入力内容そのものが合図になるため、空のブロックを置く用途に向いています。
raw:貼り付けた HTML を自作ブロックとして取り込む
クラシックエディターで書かれた記事や、他サイトの HTML をエディターに貼り付けると、エディターは中身を解析してブロックに割り振ります。既定では該当するブロックがない構造は段落やカスタム HTML になりますが、raw を登録しておくと特定のマークアップを自作ブロックとして受け取れます。
{
type: 'raw',
// 貼り付けられた要素がこのセレクターに一致したら適用
selector: 'div.notice',
// node は貼り付け内容から取り出された DOM ノード
transform: ( node ) => {
const p = node.querySelector( 'p' );
return createBlock( 'webool/notice', {
message: p ? p.innerHTML : '',
} );
},
}
セレクターで判定できない条件(属性の値を見たい、子要素の数で分けたいなど)は、selector の代わりに isMatch を書きます。isMatch はノードを受け取って真偽値を返す関数で、true を返したときだけ transform が呼ばれます。
{
type: 'raw',
isMatch: ( node ) =>
node.nodeName === 'DIV' &&
node.dataset.type === 'notice',
transform: ( node ) =>
createBlock( 'webool/notice', {
message: node.textContent.trim(),
} ),
}
shortcode:ショートコードで書かれた過去の記事を移行する
ブロックを作る前は、同じ見た目をショートコードで実現していたというケースは珍しくありません。shortcode を登録しておくと、クラシックブロックの中身をブロックへ変換したときや、ショートコードを含む本文を貼り付けたときに、ショートコードの属性を自作ブロックの属性に読み替えてくれます。
{
type: 'shortcode',
// 対象のショートコード名(角括弧は書かない)
tag: 'notice',
attributes: {
// ブロックの message 属性に何を入れるかを定義する
message: {
type: 'string',
// named にはショートコードの属性が入っている
shortcode: ( { named: { text = '' } } ) => text,
},
},
}
この定義があると、本文中の [notice text="メンテナンスのお知らせ"] が、message に「メンテナンスのお知らせ」が入ったお知らせブロックに変換されます。shortcode に渡されるオブジェクトの named には name="値" 形式の属性、numeric には名前なしで並べられた値が入ります。
ほかの type と違い、変換後のブロックは transform 関数ではなく attributes の対応表から自動的に組み立てられます。属性ごとに関数を書くのが基本の形だと覚えておくと迷いません。
複数のブロックをまとめて1つに変換する
段落を3つ選んでリストに変換すると、3項目のリストが1つできます。このように複数選択に対応するには isMultiBlock を true にします。すると transform の引数が属性のオブジェクトではなく、属性オブジェクトの配列に変わります。
{
type: 'block',
blocks: [ 'core/paragraph' ],
isMultiBlock: true,
// 選択されたブロックの数だけ要素が入った配列が渡る
transform: ( attributesArray ) =>
createBlock( 'webool/notice', {
message: attributesArray
.map( ( { content } ) => content )
.join( '<br>' ),
} ),
}
逆に、1つのブロックを複数に分けたいときは transform から配列を返します。たとえばお知らせブロックを「見出し+段落」に分解する to は、次のように書けます。
{
type: 'block',
blocks: [ 'core/heading' ],
transform: ( attributes ) => [
createBlock( 'core/heading', { content: 'お知らせ' } ),
createBlock( 'core/paragraph', {
content: attributes.message,
} ),
],
}
変換メニューに候補が出てこないとき
変換が効かないときは、たいてい次のどれかです。transforms は書き間違えていてもコンソールにエラーが出ず、静かに無視されるだけなので、上から順に確認していくのが早道です。
ブロック名が実在しているか
blocks に書く名前は 名前空間/ブロック名 の完全一致です。段落は core/paragraph、見出しは core/heading、リストは core/list です。paragraph のように名前空間を落としたり、core/lists のように綴りを間違えたりしても、その項目が無視されるだけで警告は出ません。
自信がないときはエディターのコンソールで wp.blocks.getBlockTypes().map( ( b ) => b.name ) を実行すると、登録済みブロックの名前をすべて確認できます。
登録時に transforms を渡し忘れていないか
ファイルを分けて書いたときにありがちなのが、transforms.js は作ったものの index.js の registerBlockType に渡し忘れているケースです。前述のとおり block.json に書いても効きません。edit や save と並べて渡しているかを確認してください。
変換はできるが中身が空になる
候補は出るのに変換すると内容が消える場合は、transform での属性の受け渡しがずれています。変換元と変換先で属性名は違うのが普通で、段落やリストは content、画像は url と alt、自作ブロックはこの記事の例なら message です。どちらの属性が引数に来ているのかを意識して、console.log( attributes ) で中身を確認するのが確実です。
また、変換元が属性として値を保持していない場合は取り出せません。InnerBlocks を使うブロックの中身は属性ではなく子ブロックなので、transform の第2引数で受け取る innerBlocks から扱います。
{
type: 'block',
blocks: [ 'core/group' ],
// 第2引数に変換元の子ブロックが渡ってくる
transform: ( attributes, innerBlocks ) =>
createBlock( 'webool/box', {}, innerBlocks ),
}
条件付きで候補を隠したいときは isMatch
isMatch は raw 専用ではなく、block でも使えます。変換元の属性を見て false を返すと、その変換は候補に出ません。「空の段落からは変換させない」といった制御ができます。
{
type: 'block',
blocks: [ 'core/paragraph' ],
// 中身が空の段落は変換候補に出さない
isMatch: ( { content } ) => !! content,
transform: ( { content } ) =>
createBlock( 'webool/notice', { message: content } ),
}
逆に、意図せず isMatch が false を返していて候補が消えている、という取り違えもあります。候補が出ないときは isMatch を一時的に外して切り分けてみてください。
複数の変換が競合するときは priority
同じ組み合わせに対して複数の変換が当てはまる場合、どれが使われるかは priority で決まります。フックと同じく数値が小さいほど先に評価され、既定値は 10 です。コアブロックの変換より優先したいときは 10 より小さい値を指定します。
まとめ
カスタムブロックが変換メニューに出てこないのは不具合ではなく、transforms を宣言していないためです。registerBlockType に transforms を渡し、from に「どのブロックから来られるか」、to に「どのブロックへ行けるか」を書くことで、コアブロックと同じように行き来できるようになります。block.json には書けないという点だけ押さえておけば、つまずくところはほとんどありません。
変換の中身は createBlock で新しいブロックを作って返すだけです。from では変換元、to では自分自身の属性が引数に来るので、属性名の対応付けを丁寧に書くことが実質的な作業になります。複数選択に対応するなら isMultiBlock、子ブロックを引き継ぐなら第2引数の innerBlocks を使います。
block 以外の type も便利です。よく使うブロックには prefix で入力ショートカットを用意し、クラシックエディター時代の記事を移行するなら raw や shortcode を足しておくと、既存コンテンツを手作業で貼り直す必要がなくなります。ブロックを作ったら変換まで用意する、というところまでを1セットにしておくと、実際に使う場面での使い勝手が大きく変わります。