1. ホーム
  2. WordPress

【WordPress】MediaPlaceholder の使い方|カスタムブロックに画像のドラッグ&ドロップ配置UIを作る方法

Share

画像ブロックを挿入した直後に出てくる、あの四角い枠——「アップロード」「メディアライブラリ」「URL から挿入」のボタンが並び、ファイルをドラッグ&ドロップでも受け付けるあの UI は、MediaPlaceholder というコンポーネントで作られています。自作のカスタムブロックでも @wordpress/block-editor からこれを読み込むだけで、まったく同じ見た目・同じ操作感の画像選択 UI を数行で用意できます。この記事では MediaPlaceholder の基本的な使い方から、MediaUpload との違い、主な props、選択後にツールバーから差し替えられるようにする方法までを解説します。

MediaPlaceholder は「まだ何も選ばれていない状態」の完成品 UI

MediaPlaceholder は、メディアが未選択のブロックに表示するプレースホルダー(仮置きの枠)をまるごと提供してくれるコンポーネントです。中に含まれているものを挙げると、次のようになります。

  • アイコンとタイトル、説明文が入った枠線つきのエリア
  • ローカルのファイルを選ぶ「アップロード」ボタン
  • メディアライブラリのモーダルを開くボタン
  • 枠の上にファイルを落とせるドロップゾーン
  • URL を直接入力するフォーム(有効にした場合のみ)

これらを自前で組み立てようとすると、ファイル入力の受け取り、アップロード処理、メディアライブラリの呼び出し、ドラッグ&ドロップのイベント処理と、それなりの量のコードが必要になります。MediaPlaceholder を使えば、その全部がコンポーネント1つに収まっているので、選ばれたメディアをどう保存するかだけを書けば済みます。

MediaUpload との違いと使い分け

ブロックエディターでメディアを扱うコンポーネントは複数あり、名前が似ているので混乱しやすい部分です。役割を整理すると次のようになります。

コンポーネント役割UI を持つか
MediaUploadメディアライブラリのモーダルを開く機能だけを提供する。ボタンの見た目は render で自分で書く持たない(自作)
MediaUploadCheckアップロード権限のないユーザーには中身を表示しない(代わりに fallback を出す)ラッパー持たない
MediaPlaceholder未選択時に出す、アップロード・ライブラリ・URL・ドロップゾーンが揃った完成品のプレースホルダー持つ
MediaReplaceFlow選択済みのメディアを差し替えるためのツールバーボタンとドロップダウン持つ

MediaUpload は「選択ボタンを自分でデザインしたい」ときのための低レベルな部品です。ボタンのラベルも配置も自由に決められる代わりに、プレースホルダーらしい枠や説明文、ドラッグ&ドロップは自分で用意しなければなりません。

対して MediaPlaceholder は、必要なものが最初から全部入った完成品です。実際、MediaPlaceholder の内部では MediaUpload と MediaUploadCheck が使われています。つまりMediaPlaceholder を MediaUploadCheck で包む必要はありません。すでに包まれた状態のものが返ってきます。

定番の組み合わせ方

コアの画像ブロックをはじめ、メディアを扱うブロックはおおむね同じ構成をとっています。

  • メディアが未選択のとき → MediaPlaceholder を表示する
  • メディアが選択済みのとき → 画像そのものを表示し、差し替えは BlockControls 内の MediaReplaceFlow から行う

この流れをそのまま真似すれば、コアブロックと違和感のない自作ブロックになります。この記事でもこの構成に沿って実装を組み立てていきます。

最小構成で画像ブロックを作る

ここでは「画像を1枚選んで表示するだけ」のシンプルなブロックを作りながら、MediaPlaceholder の使い方を見ていきます。@wordpress/create-block で作ったひな形をベースにした、src/ ディレクトリ構成を前提にします。

block.json で属性を用意する

選んだ画像の情報を保存するための属性(attributes)を先に決めます。最低限必要なのは、添付ファイルの ID・画像の URL・代替テキストの3つです。

src/block.json
{
    "$schema": "https://schemas.wp.org/trunk/block.json",
    "apiVersion": 3,
    "name": "mytheme/photo-card",
    "title": "フォトカード",
    "category": "media",
    "icon": "format-image",
    "description": "画像を1枚選んで表示するブロックです。",
    "textdomain": "mytheme",
    "attributes": {
        "mediaId": {
            "type": "number"
        },
        "mediaUrl": {
            "type": "string",
            "source": "attribute",
            "selector": "img",
            "attribute": "src"
        },
        "mediaAlt": {
            "type": "string",
            "source": "attribute",
            "selector": "img",
            "attribute": "alt",
            "default": ""
        }
    },
    "supports": {
        "html": false
    },
    "editorScript": "file:./index.js",
    "style": "file:./style-index.css"
}

mediaUrl と mediaAlt は source を使って、保存済み HTML の img 要素から読み戻す設定にしています。こうしておくと、保存された HTML そのものが値の保管場所になるので、ブロックコメントに URL の文字列が重複して書かれずに済みます。

一方 mediaId は source を指定していません。ID は HTML に出力しないので、ブロックコメント(<!-- wp:mytheme/photo-card {"mediaId":123} -->)に保存されます。ID を持っておくと、メディアライブラリを開いたときに現在の画像が選択済みの状態で表示できるようになります。

edit.js で MediaPlaceholder を表示する

本題の編集画面です。mediaUrl が空なら MediaPlaceholder、入っていれば画像を表示する、という分岐にします。

src/edit.js
import { __ } from '@wordpress/i18n';
import {
    BlockIcon,
    MediaPlaceholder,
    useBlockProps,
} from '@wordpress/block-editor';
import { image as imageIcon } from '@wordpress/icons';

export default function Edit( { attributes, setAttributes } ) {
    const { mediaId, mediaUrl, mediaAlt } = attributes;
    const blockProps = useBlockProps();

    // 画像が選ばれた(またはアップロードされた)ときに呼ばれる
    const onSelectMedia = ( media ) => {
        setAttributes( {
            mediaId: media.id,
            mediaUrl: media.url,
            mediaAlt: media.alt || '',
        } );
    };

    // まだ画像が無いときはプレースホルダーを出す
    if ( ! mediaUrl ) {
        return (
            <div { ...blockProps }>
                <MediaPlaceholder
                    icon={ <BlockIcon icon={ imageIcon } /> }
                    labels={ {
                        title: __( 'フォトカード', 'mytheme' ),
                        instructions: __(
                            '画像をドラッグ&ドロップするか、アップロードするか、メディアライブラリから選択してください。',
                            'mytheme'
                        ),
                    } }
                    onSelect={ onSelectMedia }
                    accept="image/*"
                    allowedTypes={ [ 'image' ] }
                    value={ { id: mediaId, src: mediaUrl } }
                />
            </div>
        );
    }

    // 画像が選ばれていればそのまま表示する
    return (
        <div { ...blockProps }>
            <img src={ mediaUrl } alt={ mediaAlt } />
        </div>
    );
}

書いているのは実質 onSelectMedia だけです。アップロードの進捗表示も、メディアライブラリのモーダルも、ドラッグ&ドロップの受け取りも、MediaPlaceholder の側で面倒を見てくれます。

icon に <BlockIcon icon={ imageIcon } /> を渡しているのは、コアブロックと同じサイズ・同じ余白でアイコンを描画するためです。icon には文字列も渡せて、その場合は Dashicon の名前として解決されます(icon="format-image" のような書き方)。@wordpress/icons の SVG アイコンを使うなら BlockIcon でくるむのが定番です。

save.js で HTML を出力する

保存側は単純です。block.json の source の指定と食い違わないように、img 要素の src と alt に属性の値を入れます。

src/save.js
import { useBlockProps } from '@wordpress/block-editor';

export default function save( { attributes } ) {
    const { mediaUrl, mediaAlt } = attributes;
    const blockProps = useBlockProps.save();

    // 画像が未選択のまま保存された場合は何も出力しない
    if ( ! mediaUrl ) {
        return null;
    }

    return (
        <div { ...blockProps }>
            <img src={ mediaUrl } alt={ mediaAlt } />
        </div>
    );
}

あとは src/index.js の registerBlockType に edit と save を渡し、npm run build(wp-scripts build)でビルドすれば、ブロック挿入時にコアの画像ブロックとそっくりなプレースホルダーが表示されるはずです。

onSelect が受け取るメディアオブジェクト

onSelect に渡ってくる media は、添付ファイルの情報をまとめたオブジェクトです。よく使うプロパティは次のとおりです。

プロパティ内容
id添付ファイルの投稿 ID(数値)。メディアライブラリで再選択するときの目印になる
urlフルサイズ画像の URL
altメディアライブラリに登録された代替テキスト。未設定なら空文字
captionキャプション。未設定なら空文字
title添付ファイルのタイトル
sizes生成済みの画像サイズごとの情報(thumbnail / medium / large など)

sizes は、それぞれのサイズについて url や width、height を持つオブジェクトです。フルサイズの画像をそのまま貼るとページが重くなるので、実際のブロックでは適切なサイズを選んで使うことが多くなります。

src/edit.js(onSelect の中身)
const onSelectMedia = ( media ) => {
    // large があれば large、無ければフルサイズにフォールバックする
    const url = media?.sizes?.large?.url ?? media.url;

    setAttributes( {
        mediaId: media.id,
        mediaUrl: url,
        mediaAlt: media.alt || '',
    } );
};

フォールバックを必ず書いてください。sizes はメディアライブラリから選んだときには揃っていますが、アップロード直後に渡ってくるオブジェクトには含まれていないことがあります。media.sizes.large.url と直接書くと、その場合にエラーで止まってしまいます。

もうひとつ知っておきたいのは、ファイルをアップロードしたとき onSelect が複数回呼ばれることです。アップロード中はプレビュー用の一時的なオブジェクト(url が blob: で始まり、id を持たない)が渡され、サーバーへの保存が完了してからあらためて正式なオブジェクトが渡ってきます。これはアップロード中でも画像をすぐ表示するための仕組みです。id が無いことを前提にした処理を書いておくか、media.id が存在するときだけ特定の処理を走らせるようにしておくと安全です。

MediaPlaceholder の主な props

よく使うものを中心に整理します。必須なのは onSelect だけで、あとはすべて省略できます。

prop型説明
onSelect関数必須。メディアが選択またはアップロードされたときに呼ばれる。multiple が有効なら配列が渡る
icon文字列 / コンポーネントタイトルの左に表示するアイコン。文字列を渡すと Dashicon として解決される
labelsオブジェクトtitle と instructions を持つオブジェクト。枠内の見出しと説明文になる
accept文字列ファイル入力の accept 属性に渡す値(例: "image/*")
allowedTypes配列アップロード・選択できるメディアの種類(例: [ 'image' ])
multiple真偽値 / 文字列複数選択を許可するか。既定は false
valueオブジェクト / 配列既定で選択状態にしたいメディア。id プロパティを持つオブジェクト(複数なら配列)
onSelectURL関数指定すると「URL から挿入」の入力欄が表示され、送信時に URL 文字列が渡る
onError関数アップロードに失敗したときにエラーメッセージを受け取る
notices要素枠内に表示する通知。withNotices の noticeUI をそのまま渡すのが定番
mediaPreview要素プレースホルダー内にプレビューとして表示する要素
disableMediaButtons真偽値true にするとボタン類を描画せず、ドロップゾーンだけになる
disableDropZone真偽値true にするとドロップゾーンを描画しない
isAppender真偽値既存のメディアに追加する用途向けの見た目に切り替える
className文字列プレースホルダーに追加するクラス名
onFilesPreUpload関数アップロード開始前に呼ばれる。ファイルの配列を受け取る
handleUpload真偽値 / 関数false を返すとアップロード処理を行わず、ファイルをそのまま onSelect に渡す

labels を省略するとどうなるか

labels は省略でき、その場合は allowedTypes の内容からタイトルと説明文が自動的に決まります。allowedTypes が [ 'image' ] だけならタイトルは「画像」、説明文は「画像をドラッグ&ドロップ、アップロード、またはライブラリから選択。」といったメッセージになります。[ 'video' ] なら「動画」、[ 'audio' ] なら「音声」です。

ここで効いてくるのが、allowedTypes に種類を1つだけ指定しているかどうかという条件です。allowedTypes を書かなかったり [ 'image', 'video' ] のように複数指定したりすると、タイトルは汎用的な「メディア」になります。自作ブロックが画像専用なら、allowedTypes={ [ 'image' ] } を指定しておくだけで文言が自然になるということです。

accept と allowedTypes はどう違うのか

名前が似ていて紛らわしいのですが、効く場所が違います。

accept は、ファイル選択ダイアログに渡される input[type="file"] の accept 属性です。OS のファイル選択画面で表示されるファイルを絞る、いわばブラウザ側のフィルターになります。

allowedTypes は WordPress 側の指定で、メディアライブラリのモーダルに表示されるメディアの種類と、アップロード時の検証に使われます。'image' のような大分類だけでなく、'image/png' のように MIME タイプを直接書くこともできます。

片方だけ指定すると、たとえば「ファイル選択では画像しか選べないのに、メディアライブラリを開くと動画も PDF も一覧に出てくる」という中途半端な状態になります。画像専用のブロックなら accept="image/*" と allowedTypes={ [ 'image' ] } の両方を書いておくのが確実です。

URL からの挿入とエラー表示を追加する

ここまでの実装にはアップロードとメディアライブラリしかありません。コアの画像ブロックにある「URL から挿入」を出すには、onSelectURL を渡します。この props を渡したときだけ、URL 入力のフォームが表示されます。

あわせてエラー表示も整えましょう。アップロードに失敗したとき(ファイルサイズ超過や許可されていない形式など)に onError が呼ばれるので、そのメッセージをプレースホルダー内に出します。ここでは @wordpress/components の withNotices を使います。これはコアのメディア系ブロックが採用している方法です。

src/edit.js
import { __ } from '@wordpress/i18n';
import {
    BlockIcon,
    MediaPlaceholder,
    useBlockProps,
} from '@wordpress/block-editor';
import { withNotices } from '@wordpress/components';
import { image as imageIcon } from '@wordpress/icons';

function Edit( {
    attributes,
    setAttributes,
    noticeUI,
    noticeOperations,
} ) {
    const { mediaId, mediaUrl, mediaAlt } = attributes;
    const blockProps = useBlockProps();

    const onSelectMedia = ( media ) => {
        setAttributes( {
            mediaId: media.id,
            mediaUrl: media?.sizes?.large?.url ?? media.url,
            mediaAlt: media.alt || '',
        } );
    };

    // 外部URLを指定されたとき。添付ファイルではないのでIDは持たない
    const onSelectURL = ( newURL ) => {
        setAttributes( {
            mediaId: undefined,
            mediaUrl: newURL,
            mediaAlt: '',
        } );
    };

    // アップロード失敗時。メッセージを通知として表示する
    const onUploadError = ( message ) => {
        noticeOperations.removeAllNotices();
        noticeOperations.createErrorNotice( message );
    };

    if ( ! mediaUrl ) {
        return (
            <div { ...blockProps }>
                <MediaPlaceholder
                    icon={ <BlockIcon icon={ imageIcon } /> }
                    labels={ { title: __( 'フォトカード', 'mytheme' ) } }
                    onSelect={ onSelectMedia }
                    onSelectURL={ onSelectURL }
                    onError={ onUploadError }
                    notices={ noticeUI }
                    accept="image/*"
                    allowedTypes={ [ 'image' ] }
                    value={ { id: mediaId, src: mediaUrl } }
                />
            </div>
        );
    }

    return (
        <div { ...blockProps }>
            <img src={ mediaUrl } alt={ mediaAlt } />
        </div>
    );
}

export default withNotices( Edit );

withNotices でくるむと、noticeUI(通知の表示要素)と noticeOperations(通知を作ったり消したりする関数のまとまり)が props として渡ってきます。noticeUI をそのまま notices に渡せば、プレースホルダーの中にエラーメッセージが表示されます。

onSelectURL が受け取るのは、メディアオブジェクトではなくURL の文字列そのものです。外部サイトの画像を指定された場合は添付ファイルとして存在しないので、mediaId はクリアしておきます。ここを揃えておかないと、次にメディアライブラリを開いたときに古い ID の画像が選択された状態で表示されてしまいます。

選択後にツールバーから差し替えられるようにする

ここまでの実装には、実は大きな穴があります。一度画像を選ぶと MediaPlaceholder が消えるため、画像を選び直す手段がありません。編集者はブロックごと削除して入れ直すしかなくなります。

これを解決するのが MediaReplaceFlow です。BlockControls の中に置くと、ブロックツールバーに「置換」ボタンが追加され、そこからメディアライブラリ・アップロード・URL 入力を呼び出せます。

src/edit.js(選択済みのときの返り値)
import {
    BlockControls,
    MediaReplaceFlow,
    useBlockProps,
} from '@wordpress/block-editor';

// ...(前半は同じ)

return (
    <>
        <BlockControls group="other">
            <MediaReplaceFlow
                mediaId={ mediaId }
                mediaURL={ mediaUrl }
                allowedTypes={ [ 'image' ] }
                accept="image/*"
                onSelect={ onSelectMedia }
                onSelectURL={ onSelectURL }
                onError={ onUploadError }
            />
        </BlockControls>
        <div { ...blockProps }>
            <img src={ mediaUrl } alt={ mediaAlt } />
        </div>
    </>
);

onSelect と onSelectURL には、MediaPlaceholder と同じ関数をそのまま渡せます。受け取るデータの形も同じなので、処理を書き分ける必要はありません。

props の名前には注意してください。MediaPlaceholder の value に相当するものが、MediaReplaceFlow では mediaId と mediaURL に分かれています。しかもURL のほうは mediaURL と大文字3文字で、mediaUrl ではありません。ここは実際によく間違える箇所です。

group="other" は、ツールバーのどのグループにボタンを置くかの指定です。コアの画像ブロックも同じ指定をしているので、これに合わせておくとボタンの並び方が他のブロックと揃います。

プレースホルダーが思ったとおりに表示されないとき

@wordpress/components からインポートしている

ビルドは通るのに画面に何も出ない、あるいは「is not a function」「undefined」といったエラーが出る場合、まずインポート元を疑ってください。MediaPlaceholder は @wordpress/block-editor のコンポーネントです。@wordpress/components にあるのは、枠だけを提供する汎用の Placeholder で、名前は似ていますが別物です。

MediaUpload / MediaUploadCheck / MediaReplaceFlow / BlockIcon / BlockControls も同じく @wordpress/block-editor です。一方 withNotices や PanelBody、Button は @wordpress/components にあります。「ブロックエディターに特化したものは block-editor、汎用の UI 部品は components」と覚えておくと迷いません。

アップロードのボタンが出ず、URL 入力だけになる

これは不具合ではなく、権限による切り替えです。MediaPlaceholder は内部で MediaUploadCheck を使っていて、メディアをアップロードする権限(upload_files)を持たないユーザーには、アップロードとメディアライブラリのボタンを表示しません。寄稿者(Contributor)権限のユーザーで確認すると、この挙動に出会います。

このとき何が表示されるかは onSelectURL を渡しているかどうかで変わります。渡していれば URL 入力のフォームだけが残り、外部の画像 URL は指定できる状態になります。渡していなければ操作するものが何も無いので、「このブロックを編集するにはメディアをアップロードする権限が必要です。」という趣旨のメッセージが表示されます。

つまり権限の切り分けは MediaPlaceholder 側で済んでいるので、自分で MediaUploadCheck を書き足す必要はありません。二重に包んでも害はありませんが、意味はありません。

画像以外のファイルまで選べてしまう

allowedTypes を指定していないと、メディアライブラリのモーダルには動画も音声も PDF も並びます。画像を前提にした save.js を書いていると、動画を選ばれた時点で表示が壊れます。

先に書いたとおり allowedTypes と accept は効く場所が違うので、両方を指定してください。加えて、allowedTypes を1種類に絞ると既定のラベルまで適切なものに変わるという副次的な効果もあります。

画像を選んでもエディターの表示が変わらない

onSelect の中で setAttributes を呼び忘れているか、保存先の属性名が block.json の attributes に定義したものと食い違っているケースがほとんどです。block.json に無いキーを setAttributes に渡しても、そのキーは保存されず黙って捨てられます。タイプミスがあっても警告が出ないので気づきにくい部分です。

切り分けとしては、onSelect の先頭に console.log( media ) を入れてみるのが早道です。ログが出ているなら選択自体は成功しているので、問題は属性の保存側にあります。ログが出ていないなら、onSelect が渡っていないか、コンポーネントが描画されていません。

保存済みの投稿を開くとブロック検証エラーになる

「このブロックには、想定されていないエラーが含まれています」という警告が出る場合は、save.js の出力と block.json の source 指定がずれています。たとえば selector に img と書いているのに save.js では figure > img の構造に変えた、といった変更が原因になります。

この記事の例のように mediaUrl が空のとき null を返す実装では、「保存されている HTML が空である」ことも検証の対象になります。あとから「画像が無くても枠だけ出したい」と save.js を書き換えると、すでに公開済みの投稿すべてが検証エラーになります。公開後に出力を変えるときは deprecated で旧バージョンを登録して移行させてください。

枠の見た目が崩れている

MediaPlaceholder の見た目を作っているスタイルは、ブロックエディターが読み込む block-editor パッケージ側のスタイルシートに含まれています。自作ブロックの editorStyle に何かを足す必要はなく、ブロックエディターの画面であれば自動的に適用されます。

それでも崩れて見えるときは、たいてい自分のブロックのスタイルが干渉しています。style.scss に div { display: flex; } のような要素セレクタを書いていると、プレースホルダー内部の要素にまで効いてしまいます。自作ブロックの CSS は必ずブロック固有のクラス(.wp-block-mytheme-photo-card など)で絞り込み、さらに編集画面だけに効かせたいスタイルは editor.scss のほうに書き分けてください。

まとめ

MediaPlaceholder は、コアの画像ブロックとまったく同じ「未選択状態の UI」を、onSelect ひとつ書くだけで自作ブロックに持ち込めるコンポーネントです。アップロード、メディアライブラリ、ドラッグ&ドロップ、権限による出し分けまでが最初から含まれているので、自分で書くのは選ばれたメディアを属性に保存する処理だけで済みます。

実装の型としては、mediaUrl が空なら MediaPlaceholder、入っていれば画像そのものを表示しつつ BlockControls に MediaReplaceFlow を置く、という流れを覚えておけば十分です。accept と allowedTypes をセットで指定すること、onSelectURL を渡さないと URL 入力が出ないこと、sizes は常にあるとは限らないこと。この3点を押さえておけば、実装でつまずく場面はぐっと減ります。

ボタンの見た目まで自分でデザインしたい特別な事情がなければ、メディア選択 UI は MediaPlaceholder に任せるのが結局いちばん早く、編集者にとっても分かりやすい形になります。まずはこの記事の最小構成をそのまま動かしてみてください。

参考ページ