BlockListBlock からは DOM が取得できない…のでそれを一応なんとかした方法

先日の記事(line-height の計算式をアップデートしたのでその解説)で、unitone は「流体タイポグラフィ(画面幅に応じて文字サイズが変化する)時に、文字サイズの変化にあわせて line-height も動的に変化するように」していることに触れました。

「流体タイポグラフィ」というのは画面幅に応じて文字サイズが流動的に変化することですが、文字サイズだけが変化して line-height が変化しない場合、行間がゆるくなったりきつくなったりしてしまいますよね。なので unitone では文字サイズの変化に応じて line-height も変化するようにしています。

この機能を実現するためには「今その要素の文字サイズが基準となる文字サイズの何倍なのか」を取得する必要があります。使用できる単位が rempx だけなら話は簡単なのですが、流動的に変化するということは vwvh を使うわけで、そうなると CSS だけで「今その要素の文字サイズが基準となる文字サイズの何倍なのか」を計算することができません。

なので unitone ではこの処理を JavaScript で実装しています。「流体タイポグラフィ」が適用されている要素の DOM を取得して、文字サイズの情報を取得し、基準となる文字サイズの何倍なのかを計算します。

この DOM の取得、フロントは全く問題なく普通に実装できるのですが、WordPress のエディターは React なのでひと工夫が必要でした。今日はそのことについて書きたいと思います(導入なげぇ…)

各ブロックに実装する場合

各ブロック毎に DOM を取得するのはそんなに難しくありません。例えばこんな感じで useRef() を使えば良いです(わかりやすくするために色々省略しているのでそのままでは動かないです><)。

const { useBlockProps } from '@wordpress/components';
const { useRef, useEffect } from '@wordpress/element';

const ref = useRef();
const blockProps = useBlockProps( { ref } );

// DOM を取得して、その文字サイズが、基準となる文字サイズの何倍なのかを計算する
useEffect( () => {
	// ブロック DOM を取得
	const target = ref?.current;

	const ownerDocument = target?.ownerDocument;
	const defaultView = ownerDocument?.defaultView;

	// 基準となる文字サイズを指定している要素の DOM を取得
	const root = ownerDocument?.querySelector( '.editor-styles-wrapper' );

	if ( ! target || ! root ) {
		return;
	}

	// ブロックの文字サイズ(実数値)を取得
	const fontSize = parseFloat(
		defaultView.getComputedStyle( target ).getPropertyValue( 'font-size' )
	);

	// 基準となる文字サイズ(実数値)を取得
	const baseFontSize = parseFloat(
		defaultView.getComputedStyle( root ).getPropertyValue( 'font-size' )
	);

	const multiple = fontSize / baseFontSize;

	// 〜あとは適当に state とかに保存したりして使う〜
}, [] );

ただ、この処理はタイポグラフィ設定を持つ全てのブロック(コアブロック含む)に持たせたいので、各ブロックごとにコードを書くことはできません。ということで、unitone では Block Filters を使って実装しています。

Block Filters を使って実装する

ブロックにも PHP と同じようにフィルターフックが用意されています。今回の場合は「今その要素の文字サイズが基準となる文字サイズの何倍なのか」を計算して DOM にその情報を属性として渡したいので、属性を追加できる editor.BlockListBlock フィルターを使います。

フックした関数で retrun する BlockListBlock は、前述したコードで言う blockProps() とほぼ同じ(と僕は認識していた)ので、ここに ref を渡せば同じようにできるんじゃ?と考えました。こんな感じ。

import { createHigherOrderComponent } from '@wordpress/compose';
const { useRef, useEffect } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';

const withFluidTypography = createHigherOrderComponent( ( BlockListBlock ) => {
	return ( props ) => {
		const ref = useRef();

		// DOM を取得して、その文字サイズが、基準となる文字サイズの何倍なのかを計算する
		useEffect( () => {
			// ブロック DOM を取得
			const target = ref?.current;

			// 以下省略...
		}, [] );

		return <BlockListBlock { ...props } ref={ ref } style={ 属性を追加 } />;
	};
}, 'withFluidTypography' );

addFilter(
	'editor.BlockListBlock',
	'unitone/fluidTypography/withFluidTypography',
	withFluidTypography
);

全く取得できない〜〜!!!(TT)

v15.5.0 の段階ではもう絶対これは無理なやつだなと思って諦めていたのですが、ふと、BlockListBlockref を渡せなくても、普通に HTML になら渡せるわけだから、空のダミー要素を追加して、そいつに ref を渡せば良いんじゃね?とひらめきました。

import { createHigherOrderComponent } from '@wordpress/compose';
const { useRef, useEffect } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';

const withFluidTypography = createHigherOrderComponent( ( BlockListBlock ) => {
	return ( props ) => {
		const { clientId } = props;

		const ref = useRef();

		// DOM を取得して、その文字サイズが、基準となる文字サイズの何倍なのかを計算する
		useEffect( () => {
			// 空 div の DOM を取得
			const dummy = ref?.current;

			// ブロック DOM を取得
			const target = ownerDocument?.getElementById( `block-${ clientId }` );

			// 以下省略...
		}, [] );

		// ダミー要素は常には必要無いので、いらないときは消えるように条件を設定
		const enableFluidTypography = 省略;

		return (
			<>
				<BlockListBlock { ...props } ref={ ref } style={ 属性を追加 } />

				( enableFluidTypography && (
					<div ref={ ref } style={{ display: 'none' }} />
				);
			</>
		);
	};
}, 'withFluidTypography' );

addFilter(
	'editor.BlockListBlock',
	'unitone/fluidTypography/withFluidTypography',
	withFluidTypography
);

これでいけました!

問題点として、DOM が1個本当の DOM の後ろに増えちゃうので、:last-child みたいな CSS セレクタでスタイリングしている場合はそれが効かなくなっちゃうというのはあります…。

コアのほうで、フック経由でも DOM が取得できるようになっていれば良いのですが…。もっと良い方法をご存じの方がいたら教えてください!

inc2734のアバター

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です