概要
とりあえずGutenbergのカスタムブロックとしてPC向けとSP向けに違う画像を表示できるブロックがあると便利かなと思い試しに作ってみたので、メモとして記事を残しておきます。
機能としては、
- PC向け/SP向けそれぞれの画像をアップできる
- アップした後画像の置換や削除することができる
- alt(代替テキスト)の入力/編集
- ブレイクポイントの編集
- リンクの設定
としています。
構造はシンプルで、これを雛形にカスタマイズしていけば良いかなというものとなっています。
なお、当方Reactなどはよくわかっていない部分が多いので、もっと良い作り方があるかと思いますが、考え方の参考になればと思います。
修正情報
[2023.10.23 修正]
apiVersion:3に対応するための修正やその他の修正をいたしました。具体的には
-
MediaReplaceFlow
コンポーネントは本来はBlockControls
に入れる仕様をエディタ本文に配置していました。
これは、apiVersion:3にしてエディタ表示がiframe
になるとMediaReplaceFlow
からメディアライブラリを起動した場合に白紙モーダルが表示されるようになってしまいます。
そのために本来の仕様の通りにBlockControls
に入れるように修正しました。 - Previewモードの追加
エディタ上でPreviewモードで表示できるようにしました。 - 置換の際にメディアライブラリを使用せずに通常のブラウザかのファイル選択からアップロードをした場合にエラーになる件の修正
[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を設定していきます。
"$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"
}
},
[省略...]
}
各値の説明
-
- pcMediaID、spMediaIDは各画像のIDを格納。
-
pcWidth、spHeight、altなどはそれぞれのwidth、height属性の値を格納。
-
pcMediaURLとspMediaURLはURLを格納させるようにします。
soruceの設定がないので、コメントデリミタに保存されます。
本来ならばこう言ったURLなどは "source": "attribute" として "attribute": "src" でsrc属性に保存した方が良いのですが、今回は画像が1枚と2枚では使うタグや属性が変わるので、汎用的に使えるようにsoruceなしで設定しています。 -
breakPointはPC向けとSP向けの表示を切り分けるviewportサイズを格納させます。
defaultで600を入れています。 -
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 (
[出力処理...]
)
}
画像をアップするボタンの設置(メディアライブラリを開くボタン)
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修正]
以下の点のため変更しています。
- 置換ボタン-
MediaReplaceFlow
コンポーネントなどをBlockControls
に移動したための追加処理 - メディアライブラリを使用しないアップロードでのエラー対処
/**
*
* @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;
}
}
画像をアップされた時の表示
次に画像をアップした際にエディタ上での表示を設定します。
画像をアップすると以下のような表示になるようにします。
/**
* 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が反映されないようです。
そのためにOption
のfocusOnMount={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(実験台)
となっています。
今後仕様が変わるかもしれませんので、注視しておく必要があります。
その他、BlockControls
、Toolbar
、Dropdown
とToolbarButton
コンポーネントを使います。
/**
* ツールバー用のリンク追加機能
*/
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
自体はマニュアルに記述がありません。
コールバック関数など
/**
* リンクブロックツールに関する関数群
*/
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)
設定した各コンポーネントなどをエディタ上に出力させます。
/** 出力 */
return (
<>
{sideBarControls}
<MyBlockControls />
<EditModeRender />
</>
);
}
保存の設定 save.js
データを保存させる設定をします。
画像タグpicture
or image
の出力部分は外部ファイルのコンポーネントとして設定します(MediaRneder.js
)。
これは後述するエディタでのPreview機能で同じようなタグを出力するので外部ファイルにしてedit.jsとsave.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;
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 エディターのスタイル
.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
を使います。
/**
* プレビュー/エディット切り替え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しておきます。
/** import components */
import MediaRender from './component/MediaRender';
EditModeRender
コンポーネントに追加
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の記載はしていませんので良い感じに自由にスタイリングして下さい。
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,
});
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 />
</>
);
}
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/>
</>
)
}
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;