STUDY

WordPress Gutenbergカスタムブロック + カスタム設定

こんにちは!3児のパパエンジニアの笹垣です。
以前こちらでWordPressのGutenbergブロック開発についてご紹介しましたが、その後実案件でカスタムブロックを使う機会があり、ブロック開発に加え、カスタム設定機能も追加したので合わせてご紹介します。

先日リリースしたARCHETYP Staffingのメディアサイト「ARCHETYP Staffing Magazine」では、インハウス・フリーランス・制作会社で働くクリエイターやエンジニア、企業のクリエイティブに対する思いなどをインタビューや対談形式で発信していますが、その記事で利用する会話モジュールが欲しいという要望から開発に至りました。

完成イメージ

はじめに

まずはじめに会話モジュールのカスタムブロックを作成します。

※カスタムブロックの開発を行う上で必要な知識や環境についてはこちらを御覧ください。

会話ブロックの追加

前回の定義リスト(dl, dt, dd)同様、会話ブロックの定義ファイルを追加し registerBlockType でWordPressに登録します。

./src/block.js// definition list
import Dl from './blocks/definition-list/dl.js';
import Dt from './blocks/definition-list/dt.js';
import Dd from './blocks/definition-list/dd.js';
const { registerBlockType } = wp.blocks;
registerBlockType( Dl.name, Dl );
registerBlockType( Dt.name, Dt );
registerBlockType( Dd.name, Dd );

# ↓ ここから下を追加

/**
* conversation
*/
import Conversation from './blocks/conversation/index.js';
registerBlockType( Conversation.name, Conversation );

続いて、会話ブロックの中身を作成します。

./src/blocks/conversation/index.jsconst { RichText, MediaUpload, MediaUploadCheck } = wp.editor;
const { TextControl, Button } = wp.components;
const { Fragment } = wp.element;
const ALLOWED_MEDIA_TYPES = [
'image/png',
'image/jpeg',
'image/gif',
];

export default {
name: 'custom-gutenberg-block/conversation',
title: '会話',
icon: 'admin-comments',
category: 'formatting',
keywords: [],
attributes: {
voice: {
type: 'string',
source: 'html',
selector: '.conversation__voice_text',
},
name: {
type: 'string',
source: 'html',
selector: '.people__name_full',
},
belong: {
type: 'string',
source: 'html',
selector: '.people__name_belong',
},
media_id: {
type: 'number',
},
media_url: {
type: 'string',
source: 'attribute',
selector: 'img',
attribute: 'src',
}
},
edit({attributes, setAttributes, className}){
const { voice, name, belong, media_id, media_url } = attributes;
const onChangeVoice = newVoice => {
setAttributes( { voice: newVoice } );
};
const onChangeName = newName => {
setAttributes( { name: newName } );
};
const onChangeBelong = newBelong => {
setAttributes( { belong: newBelong } );
};
const onSelectFile = media => {
setAttributes( {
media_url: media.url,
media_id: media.id,
} );
};
const onDeleteFile = () => {
setAttributes( {
media_url: '',
media_id: '',
} );
}
const linkSettingNoSelectedStyle = { fontSize: '12px', margin: '0 0 5px' };
return (
<div className={'conversation ' + className}>
<div className="conversation__voice">
<RichText tagName='p' value={voice} placeholder='会話文' onChange={onChangeVoice}/>
</div>
<div className="conversation__people">
<div className="people__inner">
<MediaUploadCheck>
<MediaUpload
onSelect={onSelectFile}
type="file"
allowedTypes={ ALLOWED_MEDIA_TYPES }
value={ media_id }
render={ ( { open } ) => (
<Fragment>
<p style={linkSettingNoSelectedStyle}>
{ media_id
? <Fragment><span><img src={media_url} /></span><br /><span><a href="#" onClick={onDeleteFile}>× 削除</a></span></Fragment>
: <span><img src="https://placehold.jp/124x124.png" onClick={ open } /></span>
}
</p>
<Button className={ 'components-button is-button is-default' } onClick={ open }>
アップロード
</Button>
</Fragment>
) }
/>
</MediaUploadCheck>
<p className="people__name">
<TextControl className='people__name_full' tagName='span' value={name} placeholder='氏名' onChange={onChangeName}/>
<TextControl className='people__name_belong' tagName='span' value={belong} placeholder='所属' onChange={onChangeBelong}/>
</p>
</div>
</div>
</div>
)
},
save({attributes, className}){
const { voice, name, belong, media_id, media_url } = attributes;
return (
<div className={'conversation ' + className}>
<div className="conversation__voice">
<RichText.Content className='conversation__voice_text' tagName='p' value={voice} />
</div>
<div className="conversation__people">
<div className="people__inner">
<figure><img src={media_url} /></figure>
<p className="people__name">
<span class="people__name_full">{name}</span>
<span class="people__name_belong">{belong}</span>
</p>
</div>
</div>
</div>
)
}
};

ポイントは、1行目のMediaUploadCheckMediaUploadになります。これを使うことで、カスタムブロックにメディアアップロード機能を簡単に追加することができます。

また、今回 Fragment も使っていますが、これは何も出力しないコンポーネントで、複数のコンポーネントやHTMLをまとめて一つのコンポーネントとして扱いたい時に使用します。

会話ブロックのカスタム設定

会話ブロックは、インタビューや対談で使用するものなので、登場人物が二人以上存在します。今回は、インタビュアーとインタビューイを想定し、発言者によって吹き出しのしっぽやプロフィール、氏名を左右切り替えられるようカスタム設定を追加します。

カスタム設定とは

Gutenbergはブロックを選択した際にエディター右(サイドバー)に“テキスト設定”や“色設定”、“高度な設定”という項目が表示されますが、そこに独自の設定項目を追加することができます。それをカスタム設定と言います。

完成イメージ

blocks.registerBlockType

カスタム属性を設定できるようにします。エディター起動時に一度だけ実行されます。

./src/sidebar/add-attribute.jsconst { assign } = lodash;
const isValidBlockType = ( name ) => {
const validBlockTypes = [
'custom-gutenberg/conversation'
];
return validBlockTypes.includes( name );
};
function ConversationAddAttribute( settings ) {
if ( isValidBlockType( settings.name ) ) {
settings.attributes = assign( settings.attributes, {
importantSetting: {
type: 'boolean',
},
} );
}
return settings;
}
export default ConversationAddAttribute;

editor.BlockEdit

サイドバーにカスタム設定項目を追加しUIの設定を行います。

./src/sidebar/block-control.jsconst { __ } = wp.i18n;
const { Fragment } = wp.element;
const {
PanelBody,
ToggleControl
} = wp.components;

const {
InspectorControls,
} = window.wp.editor;

const { createHigherOrderComponent } = wp.compose;

const isValidBlockType = ( name ) => {
const validBlockTypes = [
'custom-gutenberg/conversation'
];
return validBlockTypes.includes( name );
};

const ConversationBlockControl = createHigherOrderComponent( ( BlockEdit ) => {

return ( props ) => {
// isValidBlockType で指定したブロックが選択されたら表示
if ( isValidBlockType( props.name ) && props.isSelected ) {
let toggle = false;
// すでにオプション選択されていたら
// console.log(props.attributes);
if (props.attributes.className) {
// 付与されているclassを取り出す
let inputClassName = props.attributes.className;
// スペース区切りを配列に
inputClassName = inputClassName.split(' ');
// console.log(inputClassName);
// console.log(inputClassName.indexOf('conversation--right'));
if(inputClassName.indexOf('conversation--right') >= 0){
toggle = true;
}
}
return (
<Fragment>
<BlockEdit { ...props } />
<InspectorControls>
<PanelBody title="会話モジュール設定" initialOpen={ true } className="conversation-setting">
<ToggleControl
label="話し手位置"
help={ toggle ? '右' : '左' }
checked={ toggle }
onChange={ ( changeVal ) => {
let filterClassName = [];

// 高度な設定で入力している場合は追加する
if (props.attributes.className) {
// 付与されているclassを取り出す
let inputClassName = props.attributes.className;
// スペース区切りを配列に
inputClassName = inputClassName.split(' ');
// 選択されていたオプションの値を削除
filterClassName = inputClassName.filter(function(name) {
return name !== 'conversation--right';
});
}

if(changeVal === true){
// 新しく選択したオプションを追加
filterClassName.push('conversation--right');
}else{

}

// 配列を文字列に
let newClassName = filterClassName.join(' ');

toggle = changeVal;
props.setAttributes({
className: newClassName,
importantSetting: changeVal
});
} }
/>
</PanelBody>
</InspectorControls>
</Fragment>
);
}
return <BlockEdit { ...props } />;
};
}, 'addMyCustomBlockControls' );

export default ConversationBlockControl;

必要なコンポーネントを読み込んだあと、isValidBlockType関数内で、カスタム設定を有効にするブロックを設定しています。

UIの部分は、吹き出しのしっぽなどを左右に切り替えることができれば良いので、ToggleControl を使用しました。

また、「高度な設定」に直接“追加CSSクラス”を入力している可能性も考慮しています。

blocks.getSaveContent.extraProps

保存処理を設定します。

./src/sidebar/add-save-props.jsconst isValidBlockType = ( name ) => {
const validBlockTypes = [
'custom-gutenberg/conversation'
];
return validBlockTypes.includes( name );
};
function ConversationAddSaveProps( extraProps, blockType, attributes ) {
if ( isValidBlockType( blockType.name ) ) {
// なしを選択した場合はimportantSetting削除
if (attributes.importantSetting === false) {
delete attributes.importantSetting;
}
}
return extraProps;
}
export default ConversationAddSaveProps;

フィルターフックに登録

上記で作成した3つの処理をフィルターフックに登録します。

./src/block.js// definition list
import Dl from './blocks/definition-list/dl.js';
import Dt from './blocks/definition-list/dt.js';
import Dd from './blocks/definition-list/dd.js';
const { registerBlockType } = wp.blocks;
registerBlockType( Dl.name, Dl );
registerBlockType( Dt.name, Dt );
registerBlockType( Dd.name, Dd );

/**
* conversation
*/
import Conversation from './blocks/conversation/index.js';
registerBlockType( Conversation.name, Conversation );

# ↓ ここから下を追加

// Blocks settings(Sidebar)
import ConversationBlockControl from './sidebar/conversation/block-control.js';
import ConversationAddAttribute from './sidebar/conversation/add-attribute.js';
import ConversationAddSaveProps from './sidebar/conversation/add-save-props.js';
addFilter( 'editor.BlockEdit', 'custom-gutenberg/conversation-block-control', ConversationBlockControl );
addFilter( 'blocks.registerBlockType', 'custom-gutenberg/conversation-add-attribute', ConversationAddAttribute );
addFilter( 'blocks.getSaveContent.extraProps', 'custom-gutenberg/conversation-add-save-props', ConversationAddSaveProps );

完成

※スタイル(CSS)は別途定義し管理画面に読み込む必要があります。

会話モジュール(管理画面側)


会話モジュール(サイト側)

実際に会話モジュールが使用されている記事
https://staffing.archetyp.jp/magazine/dots-sakamoto/

さいごに

今回会話ブロックとそのカスタム設定を使ってみて、まだまだ管理画面を使いやすくできるなと感じました。サイト運営を行う上で管理画面の使いやすさや更新の手間を少なくすることはとても重要なことだと思うので、Gutenbergについての知見を深めたいと思います!