2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

GutenbergにpictureタグでSP/PCで違う内容の画像を表示する(アートディレクション)ブロックがないので作ってみた

Last updated at Posted at 2022-07-03

概要

とりあえずGutenbergのカスタムブロックとしてPC向けとSP向けに違う画像を表示できるブロックがあると便利かなと思い試しに作ってみたので、メモとして記事を残しておきます。

機能としては、

  1. PC向け/SP向けそれぞれの画像をアップできる
  2. アップした後画像の置換や削除することができる
  3. alt(代替テキスト)の入力/編集
  4. ブレイクポイントの編集
  5. リンクの設定

としています。

構造はシンプルで、これを雛形にカスタマイズしていけば良いかなというものとなっています。
なお、当方Reactなどはよくわかっていない部分が多いので、もっと良い作り方があるかと思いますが、考え方の参考になればと思います。

修正情報

[2023.10.23 修正]

apiVersion:3に対応するための修正やその他の修正をいたしました。具体的には

  1. MediaReplaceFlowコンポーネントは本来は BlockControlsに入れる仕様をエディタ本文に配置していました。
    これは、apiVersion:3にしてエディタ表示がiframeになるとMediaReplaceFlowからメディアライブラリを起動した場合に白紙モーダルが表示されるようになってしまいます。
    そのために本来の仕様の通りにBlockControlsに入れるように修正しました。
  2. Previewモードの追加
    エディタ上でPreviewモードで表示できるようにしました。
  3. 置換の際にメディアライブラリを使用せずに通常のブラウザかのファイル選択からアップロードをした場合にエラーになる件の修正

[2023.4.5 修正]

↓ [2023.10.23]こちらの件は6.3にて修正されたようなので内容を削除しました。また本来MediaReplaceFlowなどはBlockControlsに入れる仕様なのでその辺りの編集をしました。
Wordpress6.2においてツールバーがフローティング状態の場合にエディタ上のブロックを選択してもツールバーが表示しない現象がありました。
スクロールしたり他のボタンなどをクリックして動かす(リペント)すると表示するので、ツールバー表示するタイミングでなんらかの処理が実行されリペイントが中断してしまっているようです。
タイミングをずらしてリペイントして表示する設定を追加しました。

[2022.8.11 修正]

リンクの挿入方法でpicture内にaタグを追記していましたが、現在のHTML Living Standardではよくない書き方なので囲む方法に変更しました。
また出力部分を見直をし、関数コンポーネント化してより見通しが良いようにしました。

* コメントにて画像削除のremoveMedia関数の記載漏れの指摘がありましたので追記しました。

開発環境

@wordpress/script ver.23.0.0で構築しています。

インストールや設定、使い方やなどはGitHubのGutengerg Scriptsを参照して設定をしていきます。

ファイル構造

my-picture/
└─ build/ ←npm run start or buildで作られる
└─ node_module/
└─ src/
    ├─ block.json
    ├─ index.js
    ├─ edit.js
    ├─ save.js
    └─ component/
      └─ MediaRender.js
    └─ scss/
      ├─ editor.scss
      └─ style.scss
├─ packge.json
└─ package-lock.json

attributesの設定 block.json

必要なattributesを設定していきます。

block.json
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
[省略...]
"attributes": {
    "pcMediaID": {
        "type": "number"
    },
    "pcMediaURL": {
        "type": "string"
    },
    "pcMediaWidth": {
        "type":"number"
    },
    "pcMediaHeight": {
        "type": "number"
    },
    "spMediaID": {
        "type": "number"
    },
    "spMediaURL": {
        "type": "string"
    },
    "spMediaWidth": {
        "type":"number"
    },
    "spMediaHeight":{
        "type": "number"
    },
    "alt": {
        "type": "string",
        "source": "attribute",
        "selector": "img",
        "attribute": "alt"
    },
    "breakPoint": {
        "type":"string",
        "default":"600"
    },
    "post": {
        "type": "object"
    }
  },
[省略...]
}

各値の説明

    1. pcMediaID、spMediaIDは各画像のIDを格納。
  1. pcWidth、spHeight、altなどはそれぞれのwidth、height属性の値を格納。

  2. pcMediaURLとspMediaURLはURLを格納させるようにします。
    soruceの設定がないので、コメントデリミタに保存されます。
    本来ならばこう言ったURLなどは "source": "attribute" として "attribute": "src" でsrc属性に保存した方が良いのですが、今回は画像が1枚と2枚では使うタグや属性が変わるので、汎用的に使えるようにsoruceなしで設定しています。

  3. breakPointはPC向けとSP向けの表示を切り分けるviewportサイズを格納させます。
    defaultで600を入れています。

  4. postはリンク用(LinkControlコンポーネント)のための値が格納されます。
    例: post.url、post.opensInNewTab

index.js

editとsaveの部分は外部ファイルにして読み込ませるようにしているので、記述はとてもシンプルになります。


import {
    registerBlockType,
} from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';

import metadata from './block.json';
import Edit from './edit';
import save from './save';
import './scss/style.scss';
import './scss/editor.scss';

registerBlockType(metadata, {
    
    edit: Edit,
    save,
});

エディターの設定 edit.js

管理画面のエディターの設定はedit.jsにて行います。

必要なモジュールを読み込む

まずは必要なモジュールを@wordpress/scriptsから読み込んでおきます。

import {
    useBlockProps,
    BlockControls,
    InspectorControls,
    MediaUpload,//mediaアップロードコンポーネント
    MediaUploadCheck,//ユーザーがmediaをアップする権限があるかチェックできるコンポーネント
    MediaReplaceFlow, //mediaの置換えボタンコンポーネント
    __experimentalLinkControl as LinkControl,
} from '@wordpress/block-editor';
import {
    Button,
    TextareaControl,
    TextControl,
    PanelBody,
    ToolbarGroup,
    ExternalLink,
    ToolbarButton,
    Dropdown,
} from '@wordpress/components'
import {
    useState,
} from '@wordpress/element';

import { __ } from '@wordpress/i18n';

Editモジュールとして出力するための設定

export文を使ってモジュールとして出力するさせます。

export default function Edit(props) {
    const {
        attributes: {
            pcMediaID,
            pcMediaURL,
            pcMediaWidth,
            pcMediaHeight,
            spMediaID,
            spMediaURL,
            spMediaWidth,
            spMediaHeight,
            alt,
            breakPoint,
            post,
            isEditMode
        },
        setAttributes } = props;
    const blockProps = useBlockProps();


    [様々な関数やコンポーネントの設定などを記述...]

    return (
        [出力処理...]
    )

}

画像をアップするボタンの設置(メディアライブラリを開くボタン)

次に管理画面上では↓こんな形の入力欄を作っていきます。
スクリーンショット 2022-07-02 20.40.15.png

MediaUploadコンポーネントを使います。

/**
 * media アップロードコンポーネントを設定
 */
const mediaUpButton = (viewType) => {
    
    let setMediaID;
    let buttonText;
    if (viewType === 'PC') {
        setMediaID = pcMediaID;
        buttonText = "PC向け画像をアップロード"
    } else {
        setMediaID = spMediaID;
        buttonText = "SP向け画像をアップロード"
    }
    
    return (
        !setMediaID && (
            <div className={'media-up-button'}>
                <MediaUploadCheck>
                    <MediaUpload
                        onSelect={(media) => { onSelectImage(media, viewType) }}
                        type="image"
                        value={setMediaID}
                        render={({ open }) => (
                            <Button className={'image-button'} onClick={open}>
                                <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" role="img" aria-hidden="true" focusable="false"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM5 4.5h14c.3 0 .5.2.5.5v8.4l-3-2.9c-.3-.3-.8-.3-1 0L11.9 14 9 12c-.3-.2-.6-.2-.8 0l-3.6 2.6V5c-.1-.3.1-.5.4-.5zm14 15H5c-.3 0-.5-.2-.5-.5v-2.4l4.1-3 3 1.9c.3.2.7.2.9-.1L16 12l3.5 3.4V19c0 .3-.2.5-.5.5z"></path></svg>{buttonText}
                            </Button>
                        )}
                    />
                </MediaUploadCheck>
            </div>
        )
        
    )
};

画像アップロード時の処理はonSelect時のコールバックにて行います。
mediaには画像のIDや存在するサイズ(thumbnail、middle、large、fullなど)、URLなどその他画像に関する情報がObjectとして格納しています。
※詳しくは、console.log(media)などでチェックしてください

renderでメディアライブラリを開くためのボタンを設定できるます。
openを設定するとボタンクリック時にメディアモーダルが開きます。

参考

次にonSelectでのコールバック関数の設定をしていきます。

コールバック関数 onSelectImageの設定

[2023.10.23修正]
以下の点のため変更しています。

  1. 置換ボタン-MediaReplaceFlowコンポーネントなどをBlockControlsに移動したための追加処理
  2. メディアライブラリを使用しないアップロードでのエラー対処
/**
 * 
 * @param {obje} media アップロードした画像情報が格納されている
 * @param {string} viewType 'PC' or 'SP'
 * 
 * 画像を選択した時の処理(PC/SP共通)
 */
const onSelectImage = (media, viewType, callBack) => {
    const defSize = 'full';
    let media_sizes;
    if (!media.hasOwnProperty('sizes') && !media.hasOwnProperty('media_details')) {
        return;
    }
    if (media.hasOwnProperty('media_details')) {
        media_sizes = media['media_details']['sizes'];
    } else if (media.hasOwnProperty('sizes')) {
        media_sizes = media.sizes;
    }
    //画像選択後に動かしたいcallbackが設定されている場合の処理
    //今回はDropdownを閉じる処理を追加できるように
    if (callBack && typeof callBack === 'function') {
        callBack();
    }
    Object.keys(media_sizes).forEach((key) => {

        if (key === defSize) {
            let mediaUrl;
            //メディアライブラリを使わないアップロードでは`url`ではなく`source_url`になるのでその切り分け
            if (media_sizes[key]['url']) {
                mediaUrl = media_sizes[key]['url'];
            } else if (media_sizes[key]['source_url']) {
                mediaUrl = media_sizes[key]['source_url'];
            }
            if (viewType === "PC") {
               
                setAttributes({
                    pcMediaID: media.id,
                    pcMediaURL: mediaUrl,
                    pcMediaWidth: media_sizes[key]['width'],
                    pcMediaHeight: media_sizes[key]['height']
                });
            } else {

                setAttributes({
                    spMediaID: media.id,
                    spMediaURL: mediaUrl,
                    spMediaWidth: media_sizes[key]['width'],
                    spMediaHeight: media_sizes[key]['height']
                });
            }

        }
    });

};

引数viewtypeにてPC向けとSP向けの処理を単純に振り分けて処理するようにしています。

ボタンを表示させる処理を書いていきます。


const EditModeRender = () => {
        
    return (
        <div {...blockProps}>
            <div className='editerBox'>    
                <div className='mediaBox'>
                    <h3>PC向け画像</h3>
                    {mediaUpButton('PC')}
                    {pcMediaView}
                </div>
                <div className='mediaBox'>
                    <h3>SP向け画像</h3>
                    {mediaUpButton('SP')}
                    {spMediaView}
                </div>
            </div>
        </div>
    )
};

/** 出力 */
return (
    <>
        <EditModeRender />
    </>
    );

これにCSSを当てると上記のスクリーンショットのような形になります。
例えばこんな形でのstyle設定など


.wp-block-my-block-my-picture{//ここはblock nameで設定したクラス名にします。
    position: relative;
   
    border: 1px solid #ccc;

    &.wp-block {
        max-width: 1100px;
    }

    .editerBox {
        display: grid;
        grid-template-columns: 3fr 2fr;
        margin: 2.6em 16px 16px;
    }
    img {
        width: 100%;
        height: auto;
    }

    .mediaBox {
        display: grid;
        grid-template-columns: auto;
        grid-template-rows: 2em 1fr 32px;
        padding: 24px;
        border: 1px solid #ccc;

        &:nth-of-type(2n + 1) {
            margin-right: 10px;
        }

        h3 {
            font-size: 1rem;
        }

    }
    .imageBox {
        display:flex;
        justify-content: center;
        align-items: center;
        border: 1px solid #ccc;
       
        img {
            width: 100%;
            height: auto;
        }
    }

    &::before {
        position: absolute;
        top: 2px;
        right: 10px;
        display: block;
        padding: 2px;
        padding-bottom: 2px;
        background-color: #fff;
        color: #ccc;
        content: 'picture'
    }

    &>div {
        background-color: #fff;
    }

}

画像をアップされた時の表示

次に画像をアップした際にエディタ上での表示を設定します。
画像をアップすると以下のような表示になるようにします。
スクリーンショット 2023-10-23 12.51.22.png

/**
 * Media View
 * アップロードした画像を表示させる処理
 */
const pcMediaView = (
    pcMediaID && (
        <div class="imageBox"><img src={pcMediaURL} width={pcMediaWidth} height={pcMediaHeight} alt={alt} className={'sw-img_forPc'} /></div>
    )
);
const spMediaView = (
    spMediaID && (
        <div class="imageBox"><img src={spMediaURL} width={spMediaWidth} height={spMediaHeight} alt={alt} className={'sw-img_forSp'} /></div>
    )
    );

出力(return)に追加


const EditModeRender = () => {
        
    return (
        <div {...blockProps}>
            <div className='editerBox'>    
                <div className='mediaBox'>
                    <h3>PC向け画像</h3>
                    {mediaUpButton('PC')}
+                   {pcMediaView}
                </div>
                <div className='mediaBox'>
                    <h3>SP向け画像</h3>
                    {mediaUpButton('SP')}
+                   {spMediaView}
                </div>
            </div>
        </div>
    )
};

Block ToolbarにLink/置換/削除ボタンを追加する

画像のアップロード機能の次は置換と削除機能を追加します。

置換ボタンの設定

置換ボタンはMediaReplaceFlowコンポーネントを使います。
置換ボタンはPC用とSP用と2つのボタンが必要になります。
引数によってSP向け/PC向けを振り分けるようにしています。
また後述するように2つのボタンがToolsBarにそのまま並ぶと少しBarが長くなってしまうので、Dropdownによって二つのボタンを格納するようにします。
そうするとPopoverでの表示が残ってしまう場合があるので閉じるためのcallbackも設定しています。

/**
 * Mediaの置換ボタン
 */
const mediaReplace = (viewType, callBack) => {
    let mediaID, mediaURL,buttonName;
    if (viewType === 'PC') {
        mediaID = pcMediaID;
        mediaURL = pcMediaURL;
        buttonName = 'PC画像置換';
    } else {
        mediaID = spMediaID;
        mediaURL = spMediaURL;
        buttonName = 'SP画像置換';
    }
    return (
        mediaID &&
        (
            <div>
                <MediaUploadCheck>
                        <MediaReplaceFlow
                            mediaId={mediaID}
                            mediaURL={mediaURL}
                            name={buttonName}
                            accept="image/*"
                            onSelect={(media) => {
                                const onSelectArg = [
                                    media,
                                    viewType
                                ];
                                //置換後のcallback処理がある場合
                                if (callBack && typeof callBack === 'function') {
                                    onSelectArg.push(callBack);
                                }
                                onSelectImage(...onSelectArg);
                            }}
                        />
                </MediaUploadCheck>
            </div>
        )
    )
};

置換ボタンをDropdownに配置

DropdownをBlock Toolbarに設置する場合にrenderContent(Popover)にfocus可能なエレメント、例えばButtonなどが存在しない場合に、Block Toolsbarの他のボタンをクリックしてもPopoverが閉じない様子です。
MediaReplaceFlowコンポーネントは仕様でButtonを囲むdivにtabindex=-1が設定されていて、DropdownのPopoverが開いてもそこにfocusが反映されないようです。
そのためにOptionfocusOnMount={true}を設定してPopover自体をfocusするようにしています。
また画像選択後にメディアライブラリが閉じた後にPopoverが残ってしまわないようにmediaReplace関数にcallbackとしてonCloseを設定しています。

/**
* 置換用のDropdown
*/
const ReplaceDropdown = () => {
    //PC向け/SP向けどちらかが登録されていたらDropdownを表示
    const imageSelected = pcMediaID || spMediaID;

    return (
        imageSelected && 
        (
        <ToolbarGroup label="replaceImage">
            <Dropdown
                className='replaceImageButton'
                contentClassName='replaceImageMenu'
                //↓これがないと他のBlock ToolbarのボタンをクリックしてもPopoverが閉じない
                focusOnMount={true}
                popoverProps={{
                    placement: 'bottom-start'
                }}
                renderToggle={({ isOpen, onToggle }) => (
                    <ToolbarButton
                        onClick={() => {
                            onToggle();
                        }}
                        aria-expanded={isOpen}
                        icon={"update"}
                        label={__('Image Replace','efc-block')}
                    />
                )}
                renderContent={({ onClose }) => (
                    <>
                        {
                        //mediaReplace PC/SPどちらかの引数+onCloseを
                        //callbackに渡す
                        }
                        {mediaReplace('PC', onClose)}
                        {mediaReplace('SP', onClose)}
                    </>
                )}
            />
        </ToolbarGroup>
        )
    );
};

削除ボタンの設定

/**
 * 画像の削除ボタン
 */
const mediaDelButton = (viewType, callBack) => {
    let mediaID,buttonName;
    if (viewType === 'PC') {
        mediaID = pcMediaID;
        buttonName = 'PC画像削除';
    } else {
        mediaID = spMediaID;
        buttonName = 'SP画像削除';
    }

    return (
        mediaID && (
            <MediaUploadCheck>
                <div>
                    <Button
                        onClick={() => {
                            const onRemoveArg = [
                                viewType
                            ];
                            if (callBack && typeof callBack === 'function') {
                                onRemoveArg.push(callBack);
                            }
                            removeMedia(...onRemoveArg);
                        }}
                        className={"removeImageButton"}
                    >
                        {buttonName}
                    </Button>
                </div>
            </MediaUploadCheck>
        )
    )
};

削除ボタンのコールバック関数

※記載漏れでしたので追記しました

    /**
     * 画像削除ボタン
     * @param {stripg} viewType PC or SP
     */
    const removeMedia = (viewType) => {

        if (viewType === 'PC') {
            
            setAttributes({
                pcMediaID: '',
                pcMediaURL: '',
                pcMediaWidth: '',
                pcMediaHeight: ''
            });

        } else {

            setAttributes({
                spMediaID: '',
                spMediaURL: '',
                spMediaWidth: '',
                spMediaHeight: ''
            })
        } 

    }

削除ボタンをDropdownに配置

こちらも置換ボタンを同じように設定します。

    /**
     * 削除用のDropdown
    */
    const DeleteDropdown = () => {
        const imageSelected = pcMediaID || spMediaID;
        return (
            imageSelected && 
           (<ToolbarGroup label="deleteImage">
               <Dropdown
                   className='deleteImageButton'
                   contentClassName='deleteImageMenu'
                   focusOnMount={true}
                   popoverProps={{ placement: 'bottom-start' }}
                   renderToggle={({ isOpen, onToggle }) => (
                       <ToolbarButton
                           onClick={() => {
                               onToggle();
                           }}
                           aria-expanded={isOpen}
                           icon={"trash"}
                           label={__('Image Trash','efc-block')}
                       />
                   )}
                   renderContent={({ onClose }) => (
                       <>
                           {mediaDelButton('PC',onClose)}
                           {mediaDelButton('SP',onClose)}
                       </>
                   )}
               />
           </ToolbarGroup>)
        )
    };

リンクを設定する機能の追加

リンクを設定するのに、 LinkControlを使いました。
こちらのコンポーネントは、基本的にはツールバーに入れて使うことが多いようなのでそのようにしています。

 LinkControlはまだ__experimenta(実験台)となっています。
今後仕様が変わるかもしれませんので、注視しておく必要があります。

その他、BlockControlsToolbarDropdownToolbarButtonコンポーネントを使います。

URL設定前
LinkControlの画像1

URL設定後
LinkControlの画像2

/**
 * ツールバー用のリンク追加機能
 */
const LinkDropdown = () => {
    
    return (
        <ToolbarGroup label="linkControlbar">
            <Dropdown
                className="blockLinkButton"
                contentClassName="blockLinkBody"
                popoverProps={ { placement: 'bottom-start' } }
                renderToggle={({ isOpen, onToggle }) => (
                    <ToolbarButton
                        isPressed={values.islinkPressed}
                        onClick={onToggle}
                        aria-expanded={isOpen}
                        icon={
                            <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.6 7.2H14v1.5h1.6c2 0 3.7 1.7 3.7 3.7s-1.7 3.7-3.7 3.7H14v1.5h1.6c2.8 0 5.2-2.3 5.2-5.2 0-2.9-2.3-5.2-5.2-5.2zM4.7 12.4c0-2 1.7-3.7 3.7-3.7H10V7.2H8.4c-2.9 0-5.2 2.3-5.2 5.2 0 2.9 2.3 5.2 5.2 5.2H10v-1.5H8.4c-2 0-3.7-1.7-3.7-3.7zm4.6.9h5.3v-1.5H9.3v1.5z"></path></svg>
                        }
                    />
                )}
                renderContent={({ onClose }) =>
                (
                    <LinkControl
                        value={post}
                        onChange={(newPost) => {
                            setLink(newPost);
                        }
                        }
                        onRemove={() => {
                            removeLink();
                            onClose();
                        }
                        }
                        hasTextControl={true}
                    >
                    </LinkControl>

                )
                }
            />
        </ToolbarGroup>
    )
};

特に難しい設定をしなくとも公式の各コンポーネントのマニュアルを参照設定しています。
リンクが設定してある時にボタン自体の色を変えたかったので、そこをisPressedにて切り分けさせています。
※ただhasTextControl自体はマニュアルに記述がありません。
LinkControlの画像2

コールバック関数など

/**
 * リンクブロックツールに関する関数群
 */

let linkpressed = false;

if (post && post.hasOwnProperty('url')) {
    linkpressed = true;//リンクが設定してあるときはtrueに
}
//useStateでリンクが設定してあるかないかの管理をする
const [values, setValues] = useState({
    islinkPressed: linkpressed
});

const toggleVisible = () => {
    setValues(
        {
            ...values,
            islinkPressed: linkpressed
        }
    );
};

// リンク削除
const removeLink = () => {
    setAttributes({
        post: {},
    });
    linkpressed = false;
    toggleVisible();
}
//リンクセット
const setLink = (newPost) => {
    setAttributes({
        post: newPost,
    });
    linkpressed = true;
    setValues({
        ...values,
        islinkPressed: linkpressed
    });
}


BlockControlsに配置する

Block Toolbarに各ボタンを配置するためにBlockControlsコンポーネントを使用します。

/**
 * BlockControlsに設定したツールを入れ込む
 */
const MyBlockControls = () => {

    return (
        <BlockControls>
            <LinkDropdown />
            <ReplaceDropdown />
            <DeleteDropdown />
        </BlockControls>
    )

};

サイドバーの設定

サイドバーに以下のようにaltとブレイクポイントを設定できる入力欄を作成します。

/**
 * サイドバーのコントローラー
 */
const sideBarControls = (
    <>
        <InspectorControls>
            <PanelBody title={__('Image settings')}>
                <TextareaControl
                    label={__('Alt text (alternative text)')}
                    value={alt}
                    onChange={updateAlt}
                    help={
                        <>
                            <ExternalLink href="https://www.w3.org/WAI/tutorials/images/decision-tree">
                                {__(
                                    'Describe the purpose of the image'
                                )}
                            </ExternalLink>
                            {__(
                                'Leave empty if the image is purely decorative.'
                            )}
                        </>
                    }
                />
                <TextControl
                    label={__('Break Point(Viewport)')}
                    value={breakPoint}
                    onChange={setBreakPoint}
                    help={
                        <>
                            {__(
                                'SP向けとPC向け画像の切り替えサイズ(viewport)の数値を入力'
                            )}
                        </>
                    }
                />
            </PanelBody>
        </InspectorControls>
    </>
);

altとbrakpointそれぞれの修正時のコールバック関数の設定

単純にそれぞれ該当するattriburesの値に新しい値をセットするようにします。

    const updateAlt = (newAlt) => {
        setAttributes({ alt: newAlt });
    }

    const setBreakPoint = (newViewPort) => {
        setAttributes({ breakPoint: newViewPort })
    }

出力(return)

設定した各コンポーネントなどをエディタ上に出力させます。

edit.js
    /** 出力 */
    return (
        <>
            {sideBarControls}
            <MyBlockControls />
            <EditModeRender />
        </>
    );

}

保存の設定 save.js

データを保存させる設定をします。
画像タグpicture or imageの出力部分は外部ファイルのコンポーネントとして設定します(MediaRneder.js)。
これは後述するエディタでのPreview機能で同じようなタグを出力するので外部ファイルにしてedit.jsとsave.jsで共有化できるようにするためです。

MediaRender.js

export const MediaRender = (props) => {
  const {
    attributes: {
      pcMediaID,
      pcMediaURL,
      pcMediaWidth,
      pcMediaHeight,
      spMediaID,
      spMediaURL,
      spMediaWidth,
      spMediaHeight,
      alt,
      breakPoint,
    },
    blockProps
  } = props;
  const mediaQuery = `(min-width:${breakPoint}px)`;

  if (pcMediaID) {
      
      if (spMediaID) { //SP向けあり
          
          return (
              <picture {...blockProps}>
                  <source srcset={pcMediaURL} media={mediaQuery} />
                  <img src={spMediaURL} width={spMediaWidth} height={spMediaHeight} alt={alt} className={'sw-img_forSp'} />
              </picture>
          )
      } else {// SP向けなし

          return (
              <figure {...blockProps}>
                  <img src={pcMediaURL} width={pcMediaWidth} height={pcMediaHeight} alt={alt} className={'sw-img_forPc'} />
              </figure>
          )
      }
  } else {
      if (spMediaID) {

          return (
                <figure {...blockProps}>
                  <img src={spMediaURL} width={spMediaWidth} height={spMediaHeight} alt={alt} className={'sw-img_forSp'} />
              </figure>
          )
      }
  }
};

export default MediaRender;

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

import MediaRender from './component/MediaRender';

export default function save(props) {
    const {
        attributes: {
            post
        } } = props;
    const blockProps = useBlockProps.save();
    /**
     * 
     * @returns {JSX}
     * 画像を内包したリンクの出力関数
     */
    const LinkRender = () => {
        
        if (post && post.url) {// リンクが設定している場合
          
            if (post.opensInNewTab) {
                return (
                    <a href={post.url} className={'efc-blockLink'} target="_blank" rel="noopener">
                        <MediaRender attributes={props.attributes} blockProps={blockProps}/>
                    </a>
                )
            } else {
                return (
                    <a href={post.url} className={'efc-blockLink'}>
                        <MediaRender attributes={props.attributes} blockProps={blockProps}/>
                    </a>
                )
            }

        } else {
            return (
                <MediaRender attributes={props.attributes} blockProps={blockProps} />
            )
        }
    }

    //出力
    return (
        <>
           <LinkRender/> 
        </>
    )
}

出力は画像が一つだけの時とPC&SPとで出力を変えています。
[2022.8.11修正]
※ リンクを全体を囲むように変更し、見通しが良いように関数コンポーネントでの出力に変えました。

スタイルの調整

あとはCSSにて見た目の調整をします。
例としてサンプルを記載しておきます。

editor.scss エディターのスタイル

editor.scss
.wp-block-my-block-my-picture{//ブロック名により変わります
    position: relative;
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 2.6em 16px 16px;
    border: 1px solid #ccc;

    &.wp-block {
        max-width: 1100px;
    }

    .mediaBox {
        display: grid;
        grid-template-columns: auto;
        grid-template-rows: 2em 1fr 32px;
        padding: 24px;
        border: 1px solid #ccc;

        &:nth-of-type(2n + 1) {
            margin-right: 10px;
        }

        h3 {
            font-size: 1rem;
        }

    }
    .imageBox {
        display:flex;
        justify-content: center;
        align-items: center;
        border: 1px solid #ccc;
       
        img {
            width: auto;
            max-height: 300px;
        }
    }
    .imageEditBox {
        padding: 8px;

        .removeImageButton,
        .components-dropdown {
            border: 1px solid var(--wp-admin-theme-color)
        }
        .removeImageButton {
            margin-right: 8px;
        }

    }
    &::before {
        position: absolute;
        top: 2px;
        right: 10px;
        display: block;
        padding: 2px;
        padding-bottom: 2px;
        background-color: #fff;
        color: #ccc;
        content: 'picture or figure'
    }

    &>div {
        background-color: #fff;
    }

}
    

Previewモードの追加

このままではエディタ上ではPC向け/SP向けの画像二つが表示されています。
エディタ上でも実際の表示の確認ができるようにフロント側での表示と同じようなPreviewモードを追加をします。

PreviewButtonの追加

Edit/Previewモードの切り替えはページ初めに提示しているblock.jsonのattributesで既に設定したisEditModeを使います。

edit.js
/**
 * プレビュー/エディット切り替えtoolbar Button
 */
const PrevewButton = () => {

    return (
        (<ToolbarGroup label="viewMode">
            <ToolbarButton
                label={isEditMode ? "Preview" : "Edit"}
                icon={isEditMode ? "format-image" : "edit"}
                onClick={() => setAttributes({ isEditMode: !isEditMode })}
            />
        </ToolbarGroup>
        )
    );
};

MediaRenderコンポーネントをimportする

save.jsの時に作成したMediaRenderコンポーネントをimportをimportしておきます。

edit.js
/** import components */
import MediaRender from './component/MediaRender';

EditModeRenderコンポーネントに追加

edit.js
const EditModeRender = () => {
+    if (isEditMode) {
        
        return (
            <div {...blockProps}>
                <div className='editerBox'>    
                    <div className='mediaBox'>
                        <h3>PC向け画像</h3>
                        {mediaUpButton('PC')}
                        {pcMediaView}
                    </div>
                    <div className='mediaBox'>
                        <h3>SP向け画像</h3>
                        {mediaUpButton('SP')}
                        {spMediaView}
                    </div>
                </div>
            </div>
        )
+   } else {
+       return (
+           <div {...blockProps}>
+               <MediaRender attributes={props.attributes} />
+           </div>
+       )
+   }
};

コード全体

とりあえずこのような形で実現できました。
これをもとにもっと効率の良い書き方などがあるかもしれませんが参考程度になるかと思い記事を書きました。

editor.scssとstyl.scssの記載はしていませんので良い感じに自由にスタイリングして下さい。

index.js
import {
    registerBlockType,
} from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';

import metadata from './block.json';
import Edit from './edit';
import save from './save';
import './scss/style.scss';
import './scss/editor.scss';

registerBlockType(metadata, {
    
    edit: Edit,
    save,
});
edit.js
import {
    useBlockProps,
    BlockControls,
    InspectorControls,
    MediaUpload,//mediaアップロードコンポーネント
    MediaUploadCheck,//ユーザーがmediaをアップする権限があるかチェックできるコンポーネント
    MediaReplaceFlow, //mediaの置換えボタンコンポーネント
    __experimentalLinkControl as LinkControl,
} from '@wordpress/block-editor';
import {
    Button,
    TextareaControl,
    TextControl,
    PanelBody,
    ToolbarGroup,
    ExternalLink,
    ToolbarButton,
    Dropdown,
} from '@wordpress/components'
import {
    useState,
} from '@wordpress/element';

import { __ } from '@wordpress/i18n';

/** import components */
import MediaRender from './component/MediaRender';

export default function Edit(props) {
    const {
        attributes: {
            pcMediaID,
            pcMediaURL,
            pcMediaWidth,
            pcMediaHeight,
            spMediaID,
            spMediaURL,
            spMediaWidth,
            spMediaHeight,
            alt,
            breakPoint,
            post,
            isEditMode
        },
        setAttributes } = props;
    const blockProps = useBlockProps();

    
    /**----------------------------------------/
    /*               Functions                 /
    ------------------------------------------*/

    /**
     * 
     * @param {obje} media アップロードした画像情報が格納されている
     * @param {string} viewType 'PC' or 'SP'
     * 
     * 画像を選択した時の処理(PC/SP共通)
     */
    const onSelectImage = (media, viewType, callBack) => {
        const defSize = 'full';
        let media_sizes;
        if (!media.hasOwnProperty('sizes') && !media.hasOwnProperty('media_details')) {
            return;
        }
        if (media.hasOwnProperty('media_details')) {
            media_sizes = media['media_details']['sizes'];
        } else if (media.hasOwnProperty('sizes')) {
            media_sizes = media.sizes;
        }
        if (callBack && typeof callBack === 'function') {
            callBack();
        }
        Object.keys(media_sizes).forEach((key) => {

            if (key === defSize) {
                let mediaUrl;
                if (media_sizes[key]['url']) {
                    mediaUrl = media_sizes[key]['url'];
                } else if (media_sizes[key]['source_url']) {
                    mediaUrl = media_sizes[key]['source_url'];
                }
                if (viewType === "PC") {
                   
                    setAttributes({
                        pcMediaID: media.id,
                        pcMediaURL: mediaUrl,
                        pcMediaWidth: media_sizes[key]['width'],
                        pcMediaHeight: media_sizes[key]['height']
                    });
                    //console.log(media.sizes);
                } else {

                    setAttributes({
                        spMediaID: media.id,
                        spMediaURL: mediaUrl,
                        spMediaWidth: media_sizes[key]['width'],
                        spMediaHeight: media_sizes[key]['height']
                    });
                }

            }
        });

    };
    /**
     * 画像削除ボタン
     * @param {string} viewType PC or SP
     */
    const removeMedia = (viewType, callBack) => {

        if (viewType === 'PC') {
            
            setAttributes({
                pcMediaID: '',
                pcMediaURL: '',
                pcMediaWidth: '',
                pcMediaHeight: ''
            });

        } else {

            setAttributes({
                spMediaID: '',
                spMediaURL: '',
                spMediaWidth: '',
                spMediaHeight: ''
            })
        }
        if (callBack && typeof callBack === 'function') {
            callBack();
        }
    }
    /**
     * 
     * @param {string} newAlt altの設定
     */
    const updateAlt = (newAlt) => {
        setAttributes({ alt: newAlt });
    }
    /**
     * 
     * @param {number} newViewPort ブレイクポイントの設定
     */
    const setBreakPoint = (newViewPort) => {
        setAttributes({ breakPoint: newViewPort })
    }
    /**
     * リンクブロックツールに関する関数群
     */
    let linkpressed = false;

    if (post && post.hasOwnProperty('url')) {
        linkpressed = true;
    }
    const [values, setValues] = useState({
        //isVisible: false,
        islinkPressed: linkpressed
    });

    const toggleVisible = () => {
        //const nowVisibl = !values.isVisible;
        setValues(
            {
                ...values,
                //isVisible: nowVisibl,
                islinkPressed: linkpressed
            }
        );
    };

    const removeLink = () => {
        setAttributes({
            post: {},
        });
        linkpressed = false;
        toggleVisible();
    }
    const setLink = (newPost) => {
        setAttributes({
            post: newPost,
        });
        linkpressed = true;
        setValues({
            ...values,
            islinkPressed: linkpressed
        });
    }

    /**---------------------------------/
    /*      Componentの設定              /
    /----------------------------------*/
    /**
     * media アップロードコンポーネントを設定
     */
    const mediaUpButton = (viewType) => {
        
        let setMediaID;
        let buttonText;
        if (viewType === 'PC') {
            setMediaID = pcMediaID;
            buttonText = "PC向け画像をアップロード"
        } else {
            setMediaID = spMediaID;
            buttonText = "SP向け画像をアップロード"
        }
        
        return (
            !setMediaID && (
                <div className={'media-up-button'}>
                    <MediaUploadCheck>
                        <MediaUpload
                            onSelect={(media) => { onSelectImage(media, viewType) }}
                            type="image"
                            value={setMediaID}
                            render={({ open }) => (
                                <Button className={'image-button'} onClick={open}>
                                    <svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" role="img" aria-hidden="true" focusable="false"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM5 4.5h14c.3 0 .5.2.5.5v8.4l-3-2.9c-.3-.3-.8-.3-1 0L11.9 14 9 12c-.3-.2-.6-.2-.8 0l-3.6 2.6V5c-.1-.3.1-.5.4-.5zm14 15H5c-.3 0-.5-.2-.5-.5v-2.4l4.1-3 3 1.9c.3.2.7.2.9-.1L16 12l3.5 3.4V19c0 .3-.2.5-.5.5z"></path></svg>{buttonText}
                                </Button>
                            )}
                        />
                    </MediaUploadCheck>
                </div>
            )
            
        )
    };
    
    /**
     * Media View
     * アップロードした画像を表示させる
     */
    const pcMediaView = (
        pcMediaID && (
            <div class="imageBox"><img src={pcMediaURL} width={pcMediaWidth} height={pcMediaHeight} alt={alt} className={'sw-img_forPc'} /></div>
        )
    );
    const spMediaView = (
        spMediaID && (
            <div class="imageBox"><img src={spMediaURL} width={spMediaWidth} height={spMediaHeight} alt={alt} className={'sw-img_forSp'} /></div>
        )
    );

    /**
     * Mediaの置換ボタン
     */
    const mediaReplace = (viewType, callBack) => {
        let mediaID, mediaURL,buttonName;
        if (viewType === 'PC') {
            mediaID = pcMediaID;
            mediaURL = pcMediaURL;
            buttonName = 'PC画像置換';
        } else {
            mediaID = spMediaID;
            mediaURL = spMediaURL;
            buttonName = 'SP画像置換';
        }
        return (
            mediaID &&
            (
                <div>
                    <MediaUploadCheck>
                            <MediaReplaceFlow
                                mediaId={mediaID}
                                mediaURL={mediaURL}
                                name={buttonName}
                                accept="image/*"
                                onSelect={(media) => {
                                    const onSelectArg = [
                                        media,
                                        viewType
                                    ];
                                    if (callBack && typeof callBack === 'function') {
                                        onSelectArg.push(callBack);
                                    }
                                    onSelectImage(...onSelectArg);
                                }}
                            />
                    </MediaUploadCheck>
                </div>
            )
        )
    };
    /**
     * 画像の削除ボタン
     */
    const mediaDelButton = (viewType, callBack) => {
        let mediaID,buttonName;
        if (viewType === 'PC') {
            mediaID = pcMediaID;
            buttonName = 'PC画像削除';
        } else {
            mediaID = spMediaID;
            buttonName = 'SP画像削除';
        }

        return (
            mediaID && (
                <MediaUploadCheck>
                    <div>
                        <Button
                            onClick={() => {
                                const onRemoveArg = [
                                    viewType
                                ];
                                if (callBack && typeof callBack === 'function') {
                                    onRemoveArg.push(callBack);
                                }
                                removeMedia(...onRemoveArg);
                            }}
                            className={"removeImageButton"}
                        >
                            {buttonName}
                        </Button>
                    </div>
                </MediaUploadCheck>
            )
        )
    };

    /**
     * サイドバーのコントローラー
     */
    const sideBarControls = (
        <>
            <InspectorControls>
                <PanelBody title={__('Image settings')}>
                    <TextareaControl
                        label={__('Alt text (alternative text)')}
                        value={alt}
                        onChange={updateAlt}
                        help={
                            <>
                                <ExternalLink href="https://www.w3.org/WAI/tutorials/images/decision-tree">
                                    {__(
                                        'Describe the purpose of the image'
                                    )}
                                </ExternalLink>
                                {__(
                                    'Leave empty if the image is purely decorative.'
                                )}
                            </>
                        }
                    />
                    <TextControl
                        label={__('Break Point(Viewport)')}
                        value={breakPoint}
                        onChange={setBreakPoint}
                        help={
                            <>
                                {__(
                                    'SP向けとPC向け画像の切り替えサイズ(viewport)の数値を入力'
                                )}
                            </>
                        }
                    />
                </PanelBody>
            </InspectorControls>
        </>
    );
    /**----------------------------
     * BlockControler Tool bar setting
    ---------------------------- */
    /**
     * プレビュー/エディット切り替えtoolbar Button
     */
    const PrevewButton = () => {

        return (
            (<ToolbarGroup label="viewMode">
                <ToolbarButton
                    label={isEditMode ? "Preview" : "Edit"}
                    icon={isEditMode ? "format-image" : "edit"}
                    onClick={() => setAttributes({ isEditMode: !isEditMode })}
                />
            </ToolbarGroup>
            )
        );
    };
    /**
     * ツールバー用のリンク追加機能
     */
    const LinkDropdown = () => {
        
        return (
            <ToolbarGroup label="linkControlbar">
                <Dropdown
                    className="blockLinkButton"
                    contentClassName="blockLinkBody"
                    popoverProps={ { placement: 'bottom-start' } }
                    renderToggle={({ isOpen, onToggle }) => (
                        <ToolbarButton
                            isPressed={values.islinkPressed}
                            onClick={onToggle}
                            aria-expanded={isOpen}
                            icon={
                                <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.6 7.2H14v1.5h1.6c2 0 3.7 1.7 3.7 3.7s-1.7 3.7-3.7 3.7H14v1.5h1.6c2.8 0 5.2-2.3 5.2-5.2 0-2.9-2.3-5.2-5.2-5.2zM4.7 12.4c0-2 1.7-3.7 3.7-3.7H10V7.2H8.4c-2.9 0-5.2 2.3-5.2 5.2 0 2.9 2.3 5.2 5.2 5.2H10v-1.5H8.4c-2 0-3.7-1.7-3.7-3.7zm4.6.9h5.3v-1.5H9.3v1.5z"></path></svg>
                            }
                        />
                    )}
                    renderContent={({ onClose }) =>
                    (
                        <LinkControl
                            value={post}
                            onChange={(newPost) => {
                                setLink(newPost);
                            }
                            }
                            onRemove={() => {
                                removeLink();
                                onClose();
                            }
                            }
                            hasTextControl={true}
                        >
                        </LinkControl>
    
                    )
                    }
                />
            </ToolbarGroup>
        )
    };
    /**
     * 置換用のDropdown
     */
    const ReplaceDropdown = () => {
        const imageSelected = pcMediaID || spMediaID;
        
        return (
            imageSelected && 
            (
            <ToolbarGroup label="replaceImage">
                <Dropdown
                    className='replaceImageButton'
                    contentClassName='replaceImageMenu'
                    focusOnMount={true}
                    popoverProps={{
                        placement: 'bottom-start'
                    }}
                    renderToggle={({ isOpen, onToggle }) => (
                        <ToolbarButton
                            onClick={() => {
                                onToggle();
                            }}
                            aria-expanded={isOpen}
                            icon={"update"}
                            label={__('Image Replace','efc-block')}
                        />
                    )}
                    renderContent={({ onClose }) => (
                        <>
                            {mediaReplace('PC', onClose)}
                            {mediaReplace('SP', onClose)}
                        </>
                    )}
                />
            </ToolbarGroup>
            )
        );
    };
    /**
     * 削除用のDropdown
    */
    const DeleteDropdown = () => {
        const imageSelected = pcMediaID || spMediaID;
        return (
            imageSelected && 
           (<ToolbarGroup label="deleteImage">
               <Dropdown
                   className='deleteImageButton'
                   contentClassName='deleteImageMenu'
                   focusOnMount={true}
                   popoverProps={{ placement: 'bottom-start' }}
                   renderToggle={({ isOpen, onToggle }) => (
                       <ToolbarButton
                           onClick={() => {
                               onToggle();
                           }}
                           aria-expanded={isOpen}
                           icon={"trash"}
                           label={__('Image Trash','efc-block')}
                       />
                   )}
                   renderContent={({ onClose }) => (
                       <>
                           {mediaDelButton('PC',onClose)}
                           {mediaDelButton('SP',onClose)}
                       </>
                   )}
               />
           </ToolbarGroup>)
        )
    };
    /**
     * BlockControlsに設定したツールを入れ込む
     */
    const MyBlockControls = () => {

        return (
            <BlockControls>
                <LinkDropdown />
                <ReplaceDropdown />
                <DeleteDropdown />
                <PrevewButton />
            </BlockControls>
        )

    };
    const EditModeRender = () => {
        if (isEditMode) {
            
            return (
                <div {...blockProps}>
                    <div className='editerBox'>    
                        <div className='mediaBox'>
                            <h3>PC向け画像</h3>
                            {mediaUpButton('PC')}
                            {pcMediaView}
                        </div>
                        <div className='mediaBox'>
                            <h3>SP向け画像</h3>
                            {mediaUpButton('SP')}
                            {spMediaView}
                        </div>
                    </div>
                </div>
            )
        } else {
            return (
                <div {...blockProps}>
                    <MediaRender attributes={props.attributes} />
                </div>
            )
        }
    };

    /** 出力 */
    return (
        <>
            {sideBarControls}
            <MyBlockControls />
            <EditModeRender />
        </>
    );

}

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

import MediaRender from './component/MediaRender';

export default function save(props) {
    const {
        attributes: {
            post
        } } = props;
    const blockProps = useBlockProps.save();
    /**
     * 
     * @returns {JSX}
     * 画像を内包したリンクの出力関数
     */
    const LinkRender = () => {
        
        if (post && post.url) {// リンクが設定している場合
          
            if (post.opensInNewTab) {
                return (
                    <a href={post.url} className={'efc-blockLink'} target="_blank" rel="noopener">
                        <MediaRender attributes={props.attributes} blockProps={blockProps} />
                    </a>
                )
            } else {
                return (
                    <a href={post.url} className={'efc-blockLink'}>
                        <MediaRender attributes={props.attributes} blockProps={blockProps} />
                    </a>
                )
            }

        } else {
            return (
                <MediaRender attributes={props.attributes} blockProps={blockProps} />
            )
        }
    }

    //出力
    return (
        <>
           <LinkRender/> 
        </>
    )
}
MediaRender.js

export const MediaRender = (props) => {
  const {
    attributes: {
      pcMediaID,
      pcMediaURL,
      pcMediaWidth,
      pcMediaHeight,
      spMediaID,
      spMediaURL,
      spMediaWidth,
      spMediaHeight,
      alt,
      breakPoint,
    },
    blockProps
  } = props;
  const mediaQuery = `(min-width:${breakPoint}px)`;

  if (pcMediaID) {
      
      if (spMediaID) { //SP向けあり
          
          return (
              <picture {...blockProps}>
                  <source srcset={pcMediaURL} media={mediaQuery} />
                  <img src={spMediaURL} width={spMediaWidth} height={spMediaHeight} alt={alt} className={'sw-img_forSp'} />
              </picture>
          )
      } else {// SP向けなし

          return (
              <figure {...blockProps}>
                  <img src={pcMediaURL} width={pcMediaWidth} height={pcMediaHeight} alt={alt} className={'sw-img_forPc'} />
              </figure>
          )
      }
  } else {
      if (spMediaID) {

          return (
                <figure {...blockProps}>
                  <img src={spMediaURL} width={spMediaWidth} height={spMediaHeight} alt={alt} className={'sw-img_forSp'} />
              </figure>
          )
      }
  }
};

export default MediaRender;

2
2
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?