WordPressブロック開発 文字数を表示するブロック

ブロックエディタに追加された文字数を表示しリアルタイムで更新するカスタムブロックを作ります。

ビルドプロセスの準備

メインスクリプトをindex.jsではなく、word-counter.jsに設定したいため、package.jsonを修正する。

package.json

"build": "wp-scripts build ./src/word-counter.js",
"start": "wp-scripts start ./src/word-counter.js"

index.jsを使用している場合、デフォルトの設定のため自動的にビルドされる。ビルドコマンドにファイル名を指定する必要はない。

しかし、メインスクリプトとして別のファイルを使用したい場合は、package.jsonを書き換えて対応する。

メインスクリプトをエンキューする

add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_files_for_word_counter' );

/**
 * Enqueue the files for the format.
 */
function enqueue_files_for_word_counter() {
	$word_counter_file = plugin_dir_path( __FILE__ ) . '/build/word-counter.asset.php';

	if ( file_exists( $word_counter_file ) ) {
		$assets = include $word_counter_file;
		wp_enqueue_script(
			'word-counter',
			plugin_dir_url( __FILE__ ) . 'build/word-counter.js',
			$assets['dependencies'],
			$assets['version'],
			true
		);
	}
}

ビルドディレクトリにあるword-counter.asset.phpには次の内容が自動的に書き込まれ、word-counter.jsより読み込まれている

<?php return array('dependencies' => array(), 'version' => '31d6cfe0d16ae931b73c');

これにより、wp_enqueue_script() を使うときに、これを require() することで、JS の正しい依存関係・キャッシュ管理を自動化できる。

スロットフィルプラグインを登録する

コードをブロックエディタ内で実行するため、SlotFillシステムを使います。

SlotFillシステムとは、表示拡張のための仕組みです。

  • Slot は「表示される位置(スロット)」のこと
  • Fill は「そこに何を表示するか(コンテンツ)」

使用するSlot

PluginPostStatusInfo:エディター右側の「ステータスと公開状態」パネル(=サマリーパネル)の中に表示されるスロット。

Slotの使い方

registerPlugin()を使い、プラグイン拡張機能をエディターに登録する。

registerPlugin( 'プラグインの識別名', {
  render: () => {
    // Slot に Fill する内容を表示
  }
});

引数の意味:

  1. 第一引数(文字列):プラグインの名前(識別用)
  2. 第二引数(オブジェクト)
    • render:表示内容(React コンポーネントなど)を返す関数
import { registerPlugin } from '@wordpress/plugins';

const Render = () => {
	return null;
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );

fillを記述する

import { registerPlugin } from '@wordpress/plugins';
import { PluginPostStatusInfo } from '@wordpress/editor';

const Render = () => {
	return (
		<PluginPostStatusInfo>
			Word count information goes here
		</PluginPostStatusInfo>
	);
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );

現在挿入されているすべてのブロックを取得する

投稿本文にどんなブロックが何個入っているか把握するため、useSelectフックを使用する。

import { registerPlugin } from '@wordpress/plugins';
import { PluginPostStatusInfo } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';

const Render = () => {
	// Get the blocks from the editor.
	const blocks = useSelect( ( select ) => {}, [] );
	return (
		<PluginPostStatusInfo>
			Word count information goes here
		</PluginPostStatusInfo>
	);
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );
  • useSelect とは?
    • React のフック(Hook)の1つ
    • WordPress の「状態管理(store)」からデータを取り出すのに使う
    • @wordpress/data パッケージに入っています

useSelect の使い方

const data = useSelect( ( select ) => {
    return select( 'store-name' ).someSelector();
}, [] );

引数の意味:

  • select:WordPress に登録されている データストアを取得するための関数
  • 'store-name':取得したいデータがあるストア(例:core/block-editor
  • someSelector():特定のデータを取得するセレクター関数(例:getBlocks()

第2引数:依存配列(通常は [] でOK)

ブロックの情報を取り出す

WordPressの状態管理の仕組みはReduxをベースとしている。

  • Store(ストア):状態(データ)を保持する場所。例:core/block-editor
  • Selector(セレクター):そのストアからデータを取り出すための関数。例:getBlocks()

今回使いたいセレクター

select( 'core/block-editor' ).getBlocks()
  • core/block-editor → ブロックの状態を管理しているストア
  • getBlocks() → 現在エディターにあるすべてのブロックを配列で取得する関数

ブロックの数を表示する

/**
 * WordPress dependencies
 */
import { registerPlugin } from '@wordpress/plugins';
import { PluginPostStatusInfo } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';

const Render = () => {
	// Get the blocks from the editor.
	const blocks = useSelect(
		( select ) => select( 'core/block-editor' ).getBlocks(),
		[]
	);
	return (
		<PluginPostStatusInfo>
			{ `There are ${ blocks.length } blocks on the page` }
		</PluginPostStatusInfo>
	);
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );

リファクタリング:ハードコードよりも、ストアを直接インポート

使用するストア(store)に文字列ではなく変数でアクセスできるようにする

// 	ストア変数を使う書き方
import { store as blockEditorStore } from '@wordpress/block-editor';
select( blockEditorStore ).getBlocks()

単語数をカウントして表示する

useSelect() で エディターにあるすべてのブロック(blocks)が取得できたので、このブロック情報を元にワードカウントを計算していく。

/**
 * WordPress dependencies
 */
import { registerPlugin } from '@wordpress/plugins';
import { PluginPostStatusInfo } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { count } from '@wordpress/wordcount';

const Render = () => {
	// Get the blocks from the editor.
	const blocks = useSelect(
		( select ) => select( blockEditorStore ).getBlocks(),
		[]
	);

	const message = `There are ${ blocks.length } blocks on the page.`;
	const numberOfWordsInMessage = count( message );

	return (
		<PluginPostStatusInfo>
			{ `${ message } There are ${ numberOfWordsInMessage } words in this message` }
		</PluginPostStatusInfo>
	);
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );

@wordpress/wordcount パッケージには、テキストから単語数を数える便利な関数が用意されている。

count() 関数

import { count } from '@wordpress/wordcount';

const wordCount = count( someText, 'words' );
  • 第1引数:テキスト(string 型)
  • 第2引数'words'(もしくは 'characters' など)でカウント方法を指定
  • 戻り値:単語数などの数値

ブロック配列をテキストに変換する必要がある

blocks は配列なので、直接 count() には渡せないので、serialize()を使う・

import { serialize } from '@wordpress/blocks';

const text = serialize( blocks ); // blocks を HTML に変換

文字数が足りない場合に投稿をロックする

現在の単語数を状態として保持し、エディターの内容が変化したときにそれを監視して更新するために useState と useEffect を使う。

/**
 * WordPress dependencies
 */
import { registerPlugin } from '@wordpress/plugins';
import { PluginPostStatusInfo } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { count } from '@wordpress/wordcount';
import { serialize } from '@wordpress/blocks';
import { useEffect, useState } from '@wordpress/element';

const Render = () => {
	// Get the blocks from the editor.,
	const blocks = useSelect(
		( select ) => select( blockEditorStore ).getBlocks(),
		[]
	);
	// Define the word count display state.
	const [ wordCountDisplay, setWordCountDisplay ] = useState( 0 );

	// The required number of words;
	const requiredWordCount = 200;

	const numberOfWordsInContent = count( serialize( blocks ) );

	return (
		<PluginPostStatusInfo>
			{ `There are ${ wordCountDisplay } words in the content` }
		</PluginPostStatusInfo>
	);
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );

useState

  • 役割:状態の保存。「今の単語数を記録しておくための変数」を作る
    • wordCount:現在の単語数(状態)
    • setWordCount():それを更新する関数
    • 0:初期値(初めは単語数がゼロ)

useEffect

useEffect(() => {
    // 単語数を再計算して setWordCount する
}, [ blocks ]);
  • 副作用の処理や「変化の監視」。「内容が更新されたら単語数を再計算する仕組み」を作る。
    • この関数は、blocks(ブロックの配列)が変化するたびに再実行される
    • ブロックの内容が変わったら、単語数も再計算する

ページ内の文字数が足りない場合に投稿をロックする

文字数が十分かどうかを判断するロジックを追加する

/**
 * WordPress dependencies
 */
import { registerPlugin } from '@wordpress/plugins';
import { PluginPostStatusInfo } from '@wordpress/editor';
import { useSelect } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { count } from '@wordpress/wordcount';
import { serialize } from '@wordpress/blocks';
import { useEffect, useState } from '@wordpress/element';

const Render = () => {
	// Get the blocks from the editor.,
	const blocks = useSelect(
		( select ) => select( blockEditorStore ).getBlocks(),
		[]
	);
	// Define the word count display state.
	const [ wordCountDisplay, setWordCountDisplay ] = useState( 0 );

	// Track the changes in the content
	useEffect( () => {
		// Define a variable to track whether the post should be locked
		let lockPost = false;

		// Get the WordCount
		const currentWordCount = count( serialize( blocks ), 'words' );
		setWordCountDisplay( currentWordCount );

		// If the word count is less than the required, lock the post saving.
		if ( currentWordCount < requiredWordCount ) {
			lockPost = true;
		}

		// Lock or enable saving.
		if ( lockPost === true ) {
			console.log( 'LOCKED' );
		} else {
			console.log( 'UNLOCKED' );
		}
	}, [ blocks ] );

	// The required number of words;
	const requiredWordCount = 200;

	return (
		<PluginPostStatusInfo>
			{ `There are ${ wordCountDisplay } words in the content` }
		</PluginPostStatusInfo>
	);
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );

ポストのロックとロック解除

投稿をロックするには、内部ストアに変更を指示する必要がある。useDispatch フックを使って、WordPress のエディターの内部ストアにアクションをディスパッチ(送信)する。

/**
 * WordPress dependencies
 */
import { registerPlugin } from '@wordpress/plugins';
import { PluginPostStatusInfo } from '@wordpress/editor';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { count } from '@wordpress/wordcount';
import { serialize } from '@wordpress/blocks';
import { useEffect, useState } from '@wordpress/element';
import { store as editorStore } from '@wordpress/editor';

/**
 * Internal dependencies
 */
import WordCountDisplayComponent from './components/wordCountDisplay';

const Render = () => {
	// Get the blocks from the editor.,
	const blocks = useSelect(
		( select ) => select( blockEditorStore ).getBlocks(),
		[]
	);
	// Get the lockPostSaving and unlockPostSaving functions from the editor
	const { lockPostSaving, unlockPostSaving } = useDispatch( editorStore );

	// Define the word count display state.
	const [ wordCountDisplay, setWordCountDisplay ] = useState( 0 );

	// Track the changes in the content
	useEffect( () => {
		// Define a variable to track whether the post should be locked
		let lockPost = false;

		// Get the WordCount
		const currentWordCount = count( serialize( blocks ), 'words' );
		setWordCountDisplay( currentWordCount );

		// If the word count is less than the required, lock the post saving.
		if ( currentWordCount < requiredWordCount ) {
			lockPost = true;
		}

		// Lock or enable saving.
		if ( lockPost === true ) {
			lockPostSaving();
		} else {
			unlockPostSaving();
		}
	}, [ blocks, lockPostSaving, unlockPostSaving ] );

	// The required number of words;
	const requiredWordCount = 10;

	return (
		<PluginPostStatusInfo>
			<WordCountDisplayComponent
				wordCount={ wordCountDisplay }
				required={ requiredWordCount }
			/>
		</PluginPostStatusInfo>
	);
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );

投稿をロックまたはロック解除するメッセージを送信するには、useDispatchフックを使用する。この操作は、Redux によく似ていて、外部のコードがストアの状態を変更するために「メッセージ」を送るイメージ。

useDispatch フック

  • useDispatch フックは、@wordpress/data パッケージからインポートし、状態管理のために使う
  • useSelect と似たようなフックだが、useSelect はストアからデータを「取得」するのに対して、useDispatch はストアに「アクション」を送信するもの
  • ストア名をパラメータとして受け取る

useDispatch の使い方

const { lockPostSaving, unlockPostSaving } = useDispatch( editorStore );
  • editorStore は、WordPress のエディター用ストアの名前。これを指定することで、そのストアに関連するアクションをディスパッチできる。
  • lockPostSavingunlockPostSaving は、ストアに送信できる「アクションのメソッド」
    • lockPostSaving()投稿が保存できないようにロック
    • unlockPostSaving()ロックを解除し、投稿の保存が可能

オプションを指定して日本語対応する

日本語対応する場合はcount関数のオプションを変更する

count()

count( text, type );
  • text:文字列(今回の場合はブロックを HTML に変換したもの)
  • type(=ここでの 'words'):
    • 'words':単語数を数える(← 今使っている)
    • 'characters_including_spaces':スペースを含めた文字数を数える
    • 'characters_excluding_spaces':スペースを除いた文字数を数える
    • 'paragraphs':段落数を数える

word-counter.js

// Get the WordCount
const currentWordCount = count(
	serialize( blocks ),
	'characters_excluding_spaces'
);

完成

block.json
{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "block-developers-cookbook/word-counter",
	"version": "1.0.2",
	"title": "Word Counter",
	"category": "widgets",
	"description": "A tutorial on how to lock post saving based on the word count.",
	"textdomain": "word-counter",
	"editorScript": "file:./word-counter.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css"
}
word-counter.js
/**
 * WordPress dependencies
 */
import { registerPlugin } from '@wordpress/plugins';
import { PluginPostStatusInfo, store as editorStore } from '@wordpress/editor';
import { useSelect, useDispatch } from '@wordpress/data';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { count } from '@wordpress/wordcount';
import { serialize } from '@wordpress/blocks';
import { useEffect, useState } from '@wordpress/element';

/**
 * Internal dependencies
 */
import WordCountDisplayComponent from './components/wordCountDisplay';

const Render = () => {
	// Get the blocks from the editor.,
	const blocks = useSelect(
		( select ) => select( blockEditorStore ).getBlocks(),
		[]
	);
	// Get the lockPostSaving and unlockPostSaving functions from the editor
	const { lockPostSaving, unlockPostSaving } = useDispatch( editorStore );

	// Define the word count display state.
	const [ wordCountDisplay, setWordCountDisplay ] = useState( 0 );

	// Track the changes in the content
	useEffect( () => {
		// Define a variable to track whether the post should be locked
		let lockPost = false;

		// Get the WordCount
		const currentWordCount = count(
			serialize( blocks ),
			'characters_excluding_spaces'
		);
		setWordCountDisplay( currentWordCount );

		// If the word count is less than the required, lock the post saving.
		if ( currentWordCount < requiredWordCount ) {
			lockPost = true;
		}

		// Lock or enable saving.
		if ( lockPost === true ) {
			lockPostSaving();
		} else {
			unlockPostSaving();
		}
	}, [ blocks, lockPostSaving, unlockPostSaving ] );

	// The required number of words;
	const requiredWordCount = 3;

	return (
		<PluginPostStatusInfo>
			<WordCountDisplayComponent
				wordCount={ wordCountDisplay }
				required={ requiredWordCount }
			/>
		</PluginPostStatusInfo>
	);
};

registerPlugin( 'block-developer-cookbook-word-counter', {
	render: Render,
} );
word-counter.php
<?php

/**
 * Plugin Name:       Word Counter 2
 * Description:       A tutorial on how to lock post saving based on the word count.
 * Requires at least: 6.4
 * Requires PHP:      7.0
 * Version:           1.0.2
 * Author:            The WordPress Contributors
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       word-counter-2
 *
 * @package block-developers-cookbook
 */

namespace BlockDevelopersCookbook;

add_action('enqueue_block_editor_assets', __NAMESPACE__ . '\enqueue_files_for_word_counter');

/**
 * Enqueue the files for the format.
 */
function enqueue_files_for_word_counter() {
    $word_counter_file = plugin_dir_path(__FILE__) . '/build/word-counter.asset.php';

    if (file_exists($word_counter_file)) {
        $assets = include $word_counter_file;
        wp_enqueue_script(
            'word-counter',
            plugin_dir_url(__FILE__) . 'build/word-counter.js',
            $assets['dependencies'],
            $assets['version'],
            true
        );
    }
}
wordCountDisplay.js
/**
 *  WordPress dependencies
 */
import { PanelRow } from '@wordpress/components';
import { Icon, check, warning } from '@wordpress/icons';
import { sprintf, __ } from '@wordpress/i18n';

const WordCountDisplayComponent = ( { wordCount, required } ) => {
	const locked = wordCount < required;
	return (
		<PanelRow>
			{ sprintf(
				// Translators: %1$s is the current word count, %2$s is the required word count.
				__(
					' %1$s of %2$s required words.',
					'block-developers-cookbook'
				),
				wordCount,
				required
			) }

			{ locked ? <Icon icon={ warning } /> : <Icon icon={ check } /> }
		</PanelRow>
	);
};

export default WordCountDisplayComponent;
package.json
{
	"name": "word-counter-2",
	"version": "1.0.2",
	"description": "A tutorial on how to lock post saving based on the word count.",
	"author": "The WordPress Contributors",
	"license": "GPL-2.0-or-later",
	"main": "build/index.js",
	"scripts": {
		"build": "wp-scripts build ./src/word-counter.js",
		"format": "wp-scripts format",
		"lint:css": "wp-scripts lint-style",
		"lint:js": "wp-scripts lint-js",
		"packages-update": "wp-scripts packages-update",
		"plugin-zip": "wp-scripts plugin-zip",
		"start": "wp-scripts start ./src/word-counter.js"
	},
	"prettier": "@wordpress/prettier-config",
	"dependencies": {
		"@wordpress/icons": "^10.23.0"
	},
	"devDependencies": {
		"@wordpress/scripts": "^30.16.0"
	}
}

参考:https://blockdevelopercookbook.com/recipes/word-counter/