【WordPress】ブロックエディタでのアンカー(ID属性)重複チェックと自動付与

更新日:2024/06/26

最近のなってようやくWordPressのブロックエディターを使い始めました。
そこで早速不満が発生。

ブロック要素を複製すると、アンカー(ID属性)も複製されてしまう。
アンカーというかID属性はユニーク、つまりWebページ内では一つだけなのが原則です。
それなのに、コピペする度に増えていくのは本当に困ります。

ということで、ブロックエディタ上でID属性の重複チェックをするプログラムコードを作成しました。
ついでに、ID属性を自動セットする仕組みも組み込みました。

 

完成イメージ

今回はプラグイン用のサイドバーを追加します。
追加するとサイドバーの上にアイコンが表示されて、切り替えができるようになります。

アイコンは重複が一目でわかるように赤色のバツ印にしてあります。

Wordpressブロックエディタ プラグインサイドバー

自動補正ボタンを押すと、重複IDの別名に変更します。

重複が無いときは、アイコンが丸印になります。

Wordpressブロックエディタ プラグインサイドバー 重複なし

また、サイドバー描画時にアンカー未設定のhタグ要素に、アンカーを自動付与しています。

 

ソース

今回はjsコードとcsが必要なので、プラグインとして作成しました。

まずは、phpコードから。

id_check.php

<?php
/*
Plugin Name: id_check
Plugin URI: 
Description: ID重複のチェック
Version: 1.0
Author: けーちゃん
Author URI:
*/
?>
<?php

add_action( 'init', function () {
    wp_register_script(
        'id_check-js',
        plugins_url( 'id_check.js', __FILE__ ),
        array( 'wp-edit-post', 'wp-element', 'wp-components','wp-plugins', 'wp-editor', 'wp-data', 'react' )
    );
} );
 
add_action( 'enqueue_block_editor_assets', function() {
    wp_enqueue_script( 'id_check-js' );
    wp_enqueue_style( 'id_check-css',plugins_url( 'id_check.css', __FILE__ ) );
} );

jsとcssファイルをエンキューしてWebページに読み込ませているだけです。

次にcssファイル

id_check.css

.dashicons-id-check-ng-icon:before{
    content: "\2716"; /* ✖ */
    font-weight: bold;
    color:red;
}
.dashicons-id-check-ok-icon:before{
    content: "\3007"; /* 〇 */
    font-weight: bold;
    color:blue;
}
.is-pressed .dashicons-id-check-ok-icon:before{
    color:white;
}
.id-check-div{
    padding: 1em;
}
.id-check-div p{
    text-align: center;
}

アイコンエリアは枠だけ確保されているので、そこに疑似要素のbeforeで文字を重ねます。
画像に置き換えることもできそうですね。

次にjsコードです。

id_check.js

(  ( autoIdBloackName , autoIdPrefix )=> {
	const {wp,React} = window;
	const {createElement} = React;
	const {registerPlugin} = wp.plugins;
	const {PluginSidebar} = wp.editPost;
	const {data:wpData} = wp;

	const getAllBlock = ()=>wpData.select( "core/block-editor" ).getBlocks();

		// ID重複チェック
	const duplicateCheck = ()=>{
			// ブロック要素取得
			const blocks = getAllBlock();
				
			const [ids,noIdBlocks,flg] = blocks.reduce((r,e)=>{
				const a = e.attributes.anchor;
				if( a ) { // IDあり
					const array = r[0].get(a);
					array ? (array.push( e ),r[2] = true) : r[0].set( a , [e] );
					
				}else{ // IDなし
					if( autoIdBloackName.includes( e.name ) )
						r[1].push( e );
				}
				return r;
			},[new Map(),[],false]);
			return {ids,noIdBlocks,flg};
	}
		// IDの自動付与
	const autoID = (ids,noIdBlocks) =>{
		({ids,noIdBlocks} = ids ? {ids,noIdBlocks} : duplicateCheck());

		let idCounter = 1;
		noIdBlocks.forEach(e => {
			let id;
			do{ // 重複確認
				id = autoIdPrefix + idCounter++;
			}while(ids.has( id ));
			e.attributes.anchor = id;
			ids.set( id , null);
		});
	}
		// ID補正
	const autoCorrection = ()=>{

		const {ids} = duplicateCheck();
		mapFilter( ids.values() , e=>e.length > 1)
			.forEach( e=>{
				const currentId = e[0].attributes.anchor;
			
				let count = 1;
				for( let i = 1 ; i < e.length ; i ++ ){
					let newId;
					do{ // 重複確認
						newId = currentId +  "_" + count++;
					}while( ids.has( newId ));
					e[i].attributes.anchor = newId;
					ids.set( newId , null);
				}
			});
	}
		// Mapオブジェ簡易フィルター
	const mapFilter = (mapIterator,callBack)=>{
		const result=[];
		for( let e of mapIterator ){
			if( callBack(e) ) result.push( e );
		}
		return result;
	}

	const render = () => {
		const {ids,noIdBlocks,flg} = duplicateCheck();
		autoID( ids,noIdBlocks );

		const [count, setCount] = React.useState(0);
		const _autoCorrection = ()=>{
			autoCorrection();
			setCount( count + 1 );
		}

		const [icon,listTag,listsMenbers,buttonElement] = !flg  
			? ["id-check-ok-icon","ul",createElement( "li" , null ,"重複無し") ,[]]
			: ["id-check-ng-icon" , "ol",
				mapFilter(ids.entries(), e=>e[1].length > 1)
					.map(([key,array])=> 
						createElement("li",null,`${key} (${array.length})`)),
					createElement( "p" , null ,createElement(
						"button",{type:"button",onClick:_autoCorrection},"自動補正"))
			];
		const listElement = createElement(listTag,null,listsMenbers);
				
		return  createElement(
				PluginSidebar,
				{
					name: "id_check",
					icon: icon,
					title:"id属性重複チェック",
				},
				createElement("div",{className:"id-check-div"}
					,listElement,buttonElement),
			);
	};

	registerPlugin( "id-check-plugin-sidebar", {render:render} );

} )( 
	["core/heading"], // IDを自動付与するブロック名
	 "idx_"           // 自動付与するIDの頭文字
);

 

jsコードの解説

大前提としてWordPressのjaコードはReact上で動作しています。
そのためReactの知識が必要です。

ネット等で調べると、コード内にhtmlのタグが記述されていることがあります。
これはJSXという独自形式のため、ブラウザ上で動作しません。
そのため、環境を構築してコンパイル作業を行う必要があります。
今回は環境構築を伴わないように、JSXを使用しないコードを作成しました。

プラグインサイドバーの表示

プラグインサイドバーの表示は、registerPlugin()で行います。

registerPlugin( id文字列 , {render:render} );

第一引数は、上部のプラグインアイコンのID属性に使用されます。
第ニ引数はオブジェクトで、描画時にrenderプロパティに関連付けられた関数が呼び出されます。

render関数は、Reactの要素オブジェクトを返す必要があります。
JSXを使用すると、htmlタグのような記述方法でElementオブジェクトを作成できます。
JSXを使用しない時は、createElement()を呼び出します。

createElement(type, props, ...children)

typeは、"h1"などのタグ名または、Elementオブジェクトを返す関数を指定します。
関数の場合は、第二引数以降の値が関数に渡されます。

propsは、属性等をオブジェクトで記述します。
記述した内容の評価は、typeの値によって異なります。

childrenはtypeの子要素として使用されます。
createElement()の戻り値や文字列などを配列や、カンマ区切りで記述できます。

プラグインサイドバーにアイコンを追加するときは、typeに wp.editPost.PluginSidebarメソッドを指定します。
このメソッドは、上部にプラグインアイコンを追加して、childrenとして受け取った要素をサイドバー内に設置します。

また次の文字列を、プラグインアイコンのクラス名に追加します。

"dashicons-" + props.icon

以上のことから、次のようなコードでプラグインサイドバーに項目を追加できます。

(  (  )=> {
	const {wp,React} = window;
	const {createElement} = React;
	const {registerPlugin} = wp.plugins;
	const {PluginSidebar} = wp.editPost;

	const render = () =>  createElement(
				PluginSidebar,
				{
					name: "id_check",
					icon: "id-check-ng-icon",
					title:"id属性重複チェック",
				},
				"サイドバー内の要素"
			);

	registerPlugin( "id-check-plugin-sidebar", {render:render} );

} )( );

全てのブロックからアンカーを取得

次のコードで、ブロックエディタ上に配置された全てのブロック要素を配列で取得できます。

const allBlocks = wp.data.select( "core/block-editor" ).getBlocks();

アンカー(ID属性)は、ブロック要素のattributesオブジェクト内のanchorプロパティで確認できます。
次のコードは、全てのアンカーを取得してカンマ区切りでサイドバー上に表示しています。

const render = () =>  {
	const allBlocks = wp.data.select( "core/block-editor" ).getBlocks();
	const anchors = allBlocks.map(e=>e.attributes.anchor)
						.filter(e=>e);

	return createElement(
			PluginSidebar,
			{
				name: "id_check",
				icon: "id-check-ng-icon",
				title:"id属性重複チェック",
			},
			anchors.join(",")
		);
}

hタグ要素にアンカーを自動付与

hタグ要素は、前項のブロック取得で取得したオブジェクトのnameプロパティに "core/heading" がセットされています。
そのため、nameプロパティとattributes.anchorプロパティをチェックすることで、アンカー自動付与対象かどうかをチェックできます。

他の要素を自動付与対象にするときは、ブラウザのコンソールに wp.data.select( "core/block-editor" ).getBlocks() を入力して、nameプロパティを確認します。

webツールでname確認

サイドバーの強制レンダリング

自動補正ボタンが押された際に重複アンカーを解消していますが、解消後にレンダリング関数を呼び出してサイドバーの内容を変更する必要があります。

レンダリングを強制する場合は、まずはレンダー関数内で React.useState(0) を実行します。

const [count, setCount] = React.useState(0);

他の場所で呼び出すと、次のようなエラーがコンソールに表示されます。

Cannot read properties of null (reading 'useState')

次にボタンクリックのリスナー関数内で、setCountメソッドを呼び出します。

setCount(count+1);

このとき引数は、useState()で渡した値と異なる値を渡します。
こうすることでReactは状態が変わったと判断して、レンダリングを行います。

WordPressはHTMやCSSの知識も必要。総合的な知識を身につけよう。

更新日:2024/06/26

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。