WordPress 「メディアと文章」ブロックに吹き出しスタイルを追加

WordPressのブロックエディタに吹き出しを追加するカスタムブロックが沢山公開されてますが、基本ブロックの「メディアと文章」を利用すれば簡単に吹き出しスタイルを作れるのでやってみました (^^)

既に同様なカスタマイズの記事がありました

Gutenberg(WordPress新エディター)で吹き出しを使う方法 | Fantastech(ファンタステック)
どうも、吹き出し大好きなシェフです! 当サイトのWordPress関連の記事の中でも特に読まれているのが「吹き出し」に関する記事。 吹き出しの仕組みを紹介している記事やWordPressで簡単に吹き出しを表示する方法を紹介している記事がけっこう読まれます。 ただ、WordPress5.0からエディターが新しくなり(Gutenbergという名称)、吹き出しを…
Gutenberg(WordPress新エディター)で吹き出しを使う方法 | Fantastech(ファンタステック)

これを参考にエディタの編集画面からも簡単に使えるように JavaScript も併せて作成しました

元々は、プラグインとして作成してみたのですが(私の公開している Celtispack アドオンに含まれています)、今回は、テーマの function.php を編集して組み込む方法を紹介していきます

functions.php に追加するコード

お使いのテーマ(多くの場合はその子テーマ)の functions.php に以下の3つの関数を追加します

いきなり実サイトの functions.php を編集せずにまずはローカルのテスト環境を構築してそこで編集してください

エディタ編集時用の CSS/JS 読み込み処理

編集時用の editor-speechstyle.css と edit_speech.js ファイルを読み込んでいます

if(is_admin()){
    function fukidashi_enqueue_block_editor_assets() {
        if ( current_user_can( 'edit_posts' ) ) {
            wp_enqueue_style( 'cp-fukidashi-style', get_stylesheet_directory_uri() . "/editor-speechstyle.css");
            wp_add_inline_style( 'cp-fukidashi-style', fukidashi_style(false) );
            wp_enqueue_script( 
                'cp-fukidashi-js',
                get_stylesheet_directory_uri() . "/edit_speech.js",
                array( 'wp-hooks', 'wp-i18n', 'wp-plugins', 'wp-components', 'wp-compose', 'wp-element', 'wp-edit-post', 'wp-api-request', 'wp-data', 'wp-blocks', 'wp-editor'),
                null,
                true 
            );
        }
    }
    add_action( 'enqueue_block_editor_assets', 'fukidashi_enqueue_block_editor_assets' );
}

フロント側の吹き出し用スタイルの読み込み処理

フロント側で吹き出しスタイルに必要なCSSスタイルです。エディタ編集側でも一部のスタイルを共有しているので関数を使ってインラインで出力できるようにしています

function fukidashi_style($echo = true) {
        
$fukidashi = <<<CSS
.wp-block-media-text.cp-speech {
    grid-template-columns: 70px auto !important;
    grid-template-areas:
        "media-text-media media-text-content"
        "resizer resizer";
}
.wp-block-media-text.cp-speech.has-media-on-the-right {
    grid-template-columns: auto 70px !important;
    grid-template-areas:
        "media-text-content media-text-media"
        "resizer resizer";
}
.wp-block-media-text.cp-speech figure {
    align-self: start;
    font-size: 11px;
    line-height: 1.3;
    text-align: center;        
}
.wp-block-media-text.cp-speech figure > img {
    margin-bottom: 5px;
}    
.wp-block-media-text.cp-speech figure::after {
    content: attr(data-imglabel);
    color: inherit;
}         
.wp-block-media-text.cp-speech .wp-block-media-text__content {
    align-self: start;
    position: relative;
    padding: 1.2em;
    margin: 0 1em;  
    margin-right: 10%;
    border-radius: 10px;
    background-color: #e6ecf0; 
}
.cp-speech .wp-block-media-text__content::before {
    position: absolute;
    content: '';
    border: 10px solid transparent;
    top: 1em;
    bottom: auto;
    left: -12px;
    right: auto;
    border-left: none;
    border-right: 12px solid #e6ecf0;
}    
.wp-block-media-text.cp-speech.has-media-on-the-right .wp-block-media-text__content {
    margin: 0 1em;  
    margin-left: 10%;  
}
.cp-speech.has-media-on-the-right .wp-block-media-text__content::before {
    left: auto;
    right: -12px;
    border-left: 12px solid #e6ecf0;
    border-right: none;
}
CSS;

    $css = $fukidashi;
    if($echo === false){
        return $css;
    } else {
        echo '<style type="text/css">' . $css . '</style>';
    }
} 
add_action( 'wp_head', 'fukidashi_style', 7, 1 );

フロント側とエディタ編集側で異なるクラスでマークアップされているのに対応する為に無駄が多いですがこのようなCSS処理としています

フロント側での吹き出し用アトリビュート属性の処理

エディタ側で設定した吹き出しの背景色や画像のラベル等のアトリビュート属性をフロント側に反映させるための処理となっています

function fukidashi_attr2style($content) {
    $speechstyle = array();
    $content = preg_replace_callback( "#(<!\-\-\s+wp:media\-text\s+.*?\-\->)(.+?)(<!\-\-\s+/wp:media\-text\s)#su", function($matches) use(&$speechstyle) {
        $np = $matches[2];                
        if(preg_match('#cp\-speech#u', $matches[1] )){
            $np = str_replace( "is-stacked-on-mobile", '', $np);
            $imgClass = null;
            if(preg_match('#"imgClass":"(.+?)"#u', $matches[1], $ic )){
                $imgClass = trim($ic[1]);
                if($imgClass === 'circle-mask'){
                    if(!preg_match('#is\-style\-circle\-mask#u', $np )){
                        $np = str_replace( "wp-block-media-text__media", 'wp-block-media-text__media is-style-circle-mask', $np);
                    }
                } else {
                    if(preg_match('#is\-style\-circle\-mask#u', $np )){
                        $np = str_replace( "is-style-circle-mask", '', $np);
                    }
                }
            }
            $imgLabel = null;
            if(preg_match('#"imgLabel":"(.+?)"#u', $matches[1], $il )){
                $imgLabel = trim($il[1]);
                $np = preg_replace_callback( "#<figure(.+?)>#u", function($smatches) use(&$imgLabel) {
                    $snp = $smatches[1];
                    if(!empty($imgLabel)){
                        $snp .= ' data-imglabel="' . $imgLabel . '" ';
                    }
                    return "<figure $snp>";
                }, $np, 1);
            }

            $bkgColor = null;
            if(preg_match('#"speechBkgColor":"(.+?)"#u', $matches[1], $bkc )){
                $bkgColor = trim($bkc[1]);
                $colorId = '';
                for ($i=0; $i<strlen($bkgColor); $i++){
                    $hex = dechex( ord($bkgColor[$i]) );
                    $colorId .= substr('0'.$hex, -2);
                }
            }
            if($bkgColor){
                $rule = '.cp-speech .wp-block-media-text__content[data-speech="' . $colorId . '"]{ background-color:' . $bkgColor .  ';}';
                $rule .= '.cp-speech .wp-block-media-text__content[data-speech="' . $colorId . '"]::before{ border-right: 12px solid ' . $bkgColor .  ';border-left:none;}';
                $rule .= '.cp-speech.has-media-on-the-right .wp-block-media-text__content[data-speech="' . $colorId . '"]::before{ border-left: 12px solid ' . $bkgColor .  ';border-right:none;}';
                $speechstyle[ $colorId ] = $rule;
                $np = preg_replace_callback( "#<div(.+?wp\-block\-media\-text__content.+?)>#u", function($smatches) use(&$colorId) {
                    $snp = $smatches[1];
                    $snp .= ' data-speech="' . $colorId . '" ';
                    return "<div{$snp}>";
                }, $np, 1);                        
            }
        }
        return $matches[1] . $np . $matches[3];
    }, $content);

    $addstyle = '';
    foreach ($speechstyle as $id => $style) {
        $addstyle .= $style; 
    }
    if(!empty($addstyle)){
        $content .= "<style type='text/css'>$addstyle</style>" . PHP_EOL;
    }
    return $content;        
}
add_filter( 'the_content', 'fukidashi_attr2style', 6 );

この3つの関数をfunctions.php に追加します

次にここで読み込んでいる2つのファイルを用意します

editor-speechstyle.css ファイルについて

このファイルはエディタ編集画面に吹き出しスタイルを反映させるためのCSSファイルです

以下のコードをコピーして editor-speechstyle.css という名前のファイルとして、functions.php と同じフォルダーに保存してください

@charset "utf-8";

.wp-block-media-text .editor-media-container__resizer {
    align-self: start;
}  
.wp-block-media-text.cp-speech .block-editor-inner-blocks {
    align-self: start;
    position: relative;
    padding: 1.2em;
    margin: 0 1em;  
    margin-right: 10%;  
    border-radius: 10px;
    background-color: #e6ecf0;
}
.cp-speech .block-editor-inner-blocks::before {
    position: absolute;
    content: '';
    border: 10px solid transparent;
    top: 1em;
    bottom: auto;
    left: -12px;
    right: auto;
    border-left: none;
    border-right: 12px solid #e6ecf0;
}    
.wp-block-media-text.cp-speech.has-media-on-the-right .block-editor-inner-blocks {
    margin: 0 1em;  
    margin-left: 10%;  
}
.cp-speech.has-media-on-the-right .block-editor-inner-blocks::before {
    left: auto;
    right: -12px;
    border-left: 12px solid #e6ecf0;
    border-right: none;
}

edit_speech.js ファイルについて

このファイルはエディタ編集画面の「メディアと文章」ブロックのサイドパネルに吹き出しスタイル用の設定オプションを追加する JavaScriptファイルです

以下のコードをコピーして editor_speech.js という名前のファイルとして、functions.php と同じフォルダーに保存してください

/*
 * speech baloon extention for gutenberg media-text block.
 *
 * Version: 1.2.6
 * Author: enomoto@celtislab
 * License: GPLv2 
 */
(function () {
    
    const { Fragment, createElement, RawHTML } = wp.element;
    const { PanelBody, BaseControl, Button, ToggleControl, TextControl, ColorPalette, SelectControl } = wp.components;
    const { InspectorControls } = wp.blockEditor;
    const { createHigherOrderComponent } = wp.compose;
    const { select } = wp.data;
    const { addFilter } = wp.hooks;
    const { useState, useEffect } = React;

    //ブロックにカスタムアトリビュート追加
    function addAttribute( settings ) {
        if ( ['core/media-text'].includes( settings.name ) ) {
            settings.attributes = lodash.assign( settings.attributes, {
                speechStyle : {
                    type: 'boolean',
                    default: false,
                },
                imgClass : {
                    type: 'string',
                    default: '',
                },
                imgLabel : {
                    type: 'string',
                    default: '',
                },
                speechBkgColor : {
                    type: 'string',
                    default: null,
                },
            } );
        }
        return settings;
    }
    addFilter( 'blocks.registerBlockType', 'celtis/blocks/atttibute', addAttribute );

    //ブロック保存時のフィルター (props save)
    function addSaveProps( extraProps, blockType, attributes ) {
        if ( ['core/media-text'].includes( blockType.name ) ) {
            if ( attributes.className !== undefined) {
                attributes.speechStyle = (attributes.className.indexOf('cp-speech') !== -1)? true : false;
                if(attributes.imgClass === undefined){
                    attributes.imgClass = '';
                }
                if(attributes.imgLabel === undefined){
                    attributes.imgLabel = '';
                }
                if(attributes.speechBkgColor === undefined){
                    attributes.speechBkgColor = null;
                }
            }
        }
        return extraProps;
    }
    addFilter( 'blocks.getSaveContent.extraProps', 'celtis/blocks/saveprops', addSaveProps )

    const cpspeechControl = createHigherOrderComponent( ( BlockEdit ) => {
        return ( props ) => {
            let el = createElement;
            let attr = props.attributes;

            const imgStyle = (tag, is_style, imgClass, imgLabel) => {
                if(is_style){
                    if(tag.className.match(/block-library-media-text__media-container/)){
                        if(imgClass == 'circle-mask'){
                            if( !tag.className.match(/is-style-circle-mask/)){
                                tag.className += ' ' + 'is-style-circle-mask';                
                            }
                        } else {
                            if( tag.className.match(/is-style-circle-mask/)){
                                tag.className = tag.className.replace(/is-style-circle-mask/, '');
                            }
                        }
                    }
                    if(imgLabel !== ''){
                        tag.setAttribute('data-imglabel', imgLabel);
                    } else {
                        tag.removeAttribute('data-imglabel');
                    }                    
                }
            };

            const speechStyle = (tag, is_style, bkgColor, blockId) => {
                let speechId = tag.getAttribute('data-speech');
                if(speechId === null){
                    tag.setAttribute('data-speech', blockId );
                    speechId = blockId;
                }
                if(is_style){
                    let sttag = document.getElementById( 'speech-' + speechId );
                    let rule = '';
                    if(bkgColor){
                        rule += '.cp-speech .block-editor-inner-blocks[data-speech="' + speechId + '"]{ background-color:' + bkgColor +  ';}';
                        rule += '.cp-speech .block-editor-inner-blocks[data-speech="' + speechId + '"]::before{ border-right: 12px solid ' + bkgColor +  ';border-left:none;}';
                        rule += '.cp-speech.has-media-on-the-right .block-editor-inner-blocks[data-speech="' + speechId + '"]::before{ border-left: 12px solid ' + bkgColor +  ';border-right:none;}';
                    }
                    if(sttag === null){ 
                        let style = document.createElement("style");
                        if(rule !== ''){
                            style.id = 'speech-' + speechId;
                            style.appendChild( document.createTextNode(rule) );
                            document.getElementsByTagName("head")[0].appendChild(style);                
                        }
                    } else {
                        sttag.textContent = rule;
                    }
                }
            };

            useEffect(() => {
                if ( ['core/media-text'].includes( props.name ) ) {
                    if(props.clientId !== undefined){
                        let tag = document.querySelector('#block-' + props.clientId + ' .wp-block-media-text .block-editor-inner-blocks' );
                        if(tag !== null && attr.className !== undefined){
                            speechStyle(tag, attr.speechStyle, attr.speechBkgColor, props.clientId );
                        }
                        let fig = document.querySelector('#block-' + props.clientId + ' .wp-block-media-text .block-library-media-text__media-container' );
                        if(fig !== null && attr.className !== undefined){
                            imgStyle(fig, attr.speechStyle, attr.imgClass, attr.imgLabel );
                        }
                    }
                }
            });

            if ( ['core/media-text'].includes( props.name ) && props.isSelected ) {
                //ブロック選択時(サイドバーからのオプション設定)
                let speechStyle = (attr.speechStyle !== undefined)? attr.speechStyle : false;
                let imgClass = (attr.imgClass !== undefined)? attr.imgClass : '';
                let imgLabel = (attr.imgLabel !== undefined)? attr.imgLabel : '';
                let speechBkgColor = (attr.speechBkgColor !== undefined)? attr.speechBkgColor : null;

                let speechClass = (attr.className !== undefined)? attr.className : '';
                speechClass = speechClass.replace(/cp\-speech/, '').trim();
                if(speechStyle){ speechClass += ' cp-speech'; }

                const colorpalette = select('core/editor').getEditorSettings().colors;

                return (
                    el( Fragment,
                        {},
                        el( BlockEdit,
                            props
                        ),
                        el( InspectorControls,
                            {},
                            el( PanelBody,
                                { title: '[Celtis] 吹き出しスタイル',
                                  initialOpen: false,
                                },
                                el( ToggleControl, 
                                    { label: '吹き出し',
                                      help : '吹き出しスタイルを有効化します',
                                      checked : speechStyle,
                                      onChange : ( value ) => {
                                          speechClass = speechClass.replace(/cp\-speech/, '').trim();
                                          if(value){ 
                                            speechClass += ' cp-speech';
                                          }
                                          props.setAttributes({ className: speechClass, speechStyle: value });
                                      }                                
                                    }
                                ),
                                el( RawHTML,
                                    {},
                                    '<div style="font-size:12px; padding:8px; margin:-8px 0 16px; background-color:rgba(112,195,112,0.2); border:1px solid #70c370; border-radius:4px;">※吹き出しスタイルを常用する場合は再利用ブロックに登録すると使いやすくなるのでお勧めです</div>'
                                ),
                                el( SelectControl,
                                    { label: '画像スタイル',
                                      help : '※画像は 70px で縮小表示されます',
                                      value : imgClass,
                                      options : [
                                        {label: 'デフォルト',  value: '' },
                                        {label: 'サークルマスク', value: 'circle-mask' },
                                      ],
                                      onChange : ( value ) => { 
                                          props.setAttributes({ imgClass: value });
                                      }                                
                                    }
                                ),
                                el( TextControl, 
                                    { label: '画像に表示するラベル',
                                      value: imgLabel,
                                      onChange : ( text ) => {
                                          props.setAttributes({ imgLabel: text });
                                      }
                                    }
                                ),
                                el( BaseControl,
                                    { label: '吹き出し背景色',
                                    },                        
                                    el( ColorPalette, 
                                        { colors: colorpalette,
                                          value: speechBkgColor,
                                          onChange : ( value ) => {
                                              props.setAttributes({ speechBkgColor: value });
                                          }
                                       }
                                    )
                                ),
                            )
                        )
                    )                    
                )
            }
            return el(BlockEdit, props)
        };
    }, 'cpspeechControl' );
    addFilter( 'editor.BlockEdit', 'celtis/blocks/cpspeech', cpspeechControl );
    
}());

動作確認

上記の作業が終わったら編集した functions.php, editor-speechstyle.css, edit_speech.js の3つのファイルをテーマに設置して動作することを確認します

ローカル環境で動作に問題がないことを確認してから実サイトにコピーしてください

ブロックエディタの編集画面を開き、メディアと文章のブロックを追加します

  1. メディアに適当な顔の画像等をアップロードします
  2. 文章に適当なセリフ等を入力します

こんな感じになります

サイドパネルから吹き出しスタイルを指定

サイドパネルに以下のような項目が追加されているので、吹き出しをクリック(有効化)して、画像のスタイルや吹き出し背景色を指定します

画像の左右入れ替えは、メディアと文章ブロックの機能を利用して行います

吹き出しで会話風にするには、メディアと文章ブロックを追加して同様に吹き出しスタイルを指定していきます

背景色のカラーパレットはお使いのテーマによって異なります。この画像は、私の作成した Celtis Speedy テーマを使った場合の表示です。希望の色がない場合はカスタムカラーを使って色を指定してください

吹き出しスタイルを常用する場合は再利用ブロックに登録すると使いやすくなるのでおすすめです

投稿を公開して表示を確認

投稿記事を保存して公開します

投稿を表示して、ちゃんと吹き出しが表示されていればOKです

ここまで問題なく表示されることを確認したら、FTP等を使い3つのファイルを実サイトの テーマ内へコピーすれば使用できるようになります

以上、メディアと文章ブロックを利用した吹き出しについて紹介いたしました


まとめ記事紹介

go-to-top