【Gutenberg】Format APIとFontSizePicker+Popoverで個別フォントサイズ設定

# 概要

Wordpressの段落（rich-text）には選択した部分のサイズのみ変更する機能がありません。
しかし`Format API`を使用すれば書式ツールバーにその機能を追加することができるとのこと。

また通常では+ / - ボタンのように大きくや小さくなどのカスタムボタンにすると割と簡単にできるのですが、今回は`FontSizePicker`を使い複数のサイズを選択できるようにしたいと思いやってみました。

![スクリーンショット 2023-11-20 20.39.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34746/454c8714-e80e-7adf-5823-73c3501a3868.png)

:::note warn
2024.08.11修正しました
WordPresss6.6になり、新しい React JSX トランスフォーム(React 17以降）になったりで@wordpress/scriptでのbuildも@wordpress/scriptバージョン28.x.xで行わないとエラーになります。
**参考URL**
https://make.wordpress.org/core/2024/06/06/jsx-in-wordpress-6-6/

また、エディタもiframe化などが行われてきているので、その辺りの修正を追加しました。
:::
# 要件定義

ざっくりと要件定義としては

1. `Format API`にて選択部分のテキストの個別サイズ変更を`FontSizePicker`を使って選択できるようにする
2. `FontSizePicker`は他のツール（ハイライト）などと同じように`Popover`にて選択テキスト近くに表示させる

# 仕様

## 仕様策定のため調べたこと

まずは仕様の策定ですが仕様の策定のため調べたことを箇条書きに記載します。
バグや仕様上どうしても上手く行かない方法などがあるのでその辺りを含めてい記述します。

- Format APIの`registerFormatType`メソッドを使ってカスタムフォーマットを登録する必要がある
- 選択したテキストにカスタムフォーマットを反映させるには、`applyFormat`メソッドもしくは`toggleFormat`メソッドにて行う。
- `applyFormat`メソッド / `toggleFormat`メソッドを実行するとエディタ全体（Popover含め）でリペイントもしくはリフローが発生する場合がある。特にFont Sizeの変更はリフローが発生する。またFontSizePicker自体もリフローされてしまう様子。
- FontSizePickerの基本的な使い方としては、onChange関数にて変更があった際に`applyFormat`メソッド / `toggleFormat`メソッドを実行させる。
- ただし上記の方法でサイズボタンクリックですぐに`applyFormat`メソッド / `toggleFormat`メソッドにて反映させると選択ボタンのアニメーションが必ず左端から飛んでくるようになりバタバタした形で不恰好になる

- 上記を簡単に避けるにはサイズボタンをクリックでPopoverを閉じてしまえば良いが、どの大きさを選択したいか迷っているときは何度もFontSizePickerを表示させないといけなくなり、使い勝手が悪くなる。
- 別の方法としてはサイズボタンをクリックしただけでは`applyFormat`メソッド / `toggleFormat`メソッドを実行せずに仮の方法、例えばuseRefなどを用意て選択する値を保存しておき、決定ボタンなどをクリックで`applyFormat`メソッド / `toggleFormat`メソッドして決定させるという方法であればアニメーションもバタバタせずにいける
- またWP6.4.1では解決しているがWP6.3.xでは初めに設定をするときPopoverが左端に飛んでしまうというバグがありその対策としてはあらかじめ初期値を設定した状態でPopoverを起動すると左端に飛ぶことはなくなる。
[https://github.com/WordPress/gutenberg/pull/54736](https://github.com/WordPress/gutenberg/pull/54736) (参考）
- また6.3.xの場合のFontSizePickerのonChangeにおいて、すでに値が保存されている場合にいったん別サイズのボタンをクリック後に元サイズのボタンをクリックしても値が取れない。これは6.4.xでは値が取れる
    

## 仕様の策定

1. `RichTextToolbarButton`にカスタムボタンを追加しそれをクリックするとPopoverにてFontSizePickerが表示する
2. FontSizePickerはサイズ選択ボタンのみ使用する。
3. ボタンクリックアニメーションがバタバタするので、サイズボタンクリックでは、サイズ情報は仮保存とし、”決定”ボタンクリックでデータ保存`applyFormet`メソッドを実行する
4. 上記仕様のため初回の`RichTextToolbarButton`にて追加したカスタムボタンクリック時にPopover前に初期値（font-size:1em)で登録する。
5. “キャンセル”ボタンの仕様
    1. キャンセルボタンは、初期値のまま他のサイズを選択していない状態でクリックした場合はremoveFormatを実行して解除する
    2. すでに値が反映されている状態での再編集の場合は、新たに選択した値は反映されずに前回のままにする
6. “クリア”ボタンの仕様
    1. クリアボタンを選択したテキスト部分のFormatを解除する
7. 6.3.xの場合のFontSizePickerのonChangeにおいて、すでに値が保存されている場合にいったん別サイズのボタンをクリック後に元サイズのボタンをクリックしても値が取れない場合の対処を追加
8.  ユーザーによるイレギュラーな操作への対応
    1. 初回の選択で何も選択せず、”キャンセル”ボタンもクリックせずに他ブロックや同ブロックの別箇所クリックをするとPopoverが閉じてしまう場合の処理の追加

という仕様で作成。

# 実装

`@wordpress/script`を使用して実装していきます。
下方にコード全体を入れています。

## 使用するコンポーネントの読み込み

```diff_jsx
import { useState, useEffect, useRef } from "@wordpress/element";
import { Popover, Button,FontSizePicker } from "@wordpress/components";
import { registerFormatType, applyFormat, removeFormat, getActiveFormat, 
+ useAnchor
} from '@wordpress/rich-text';
import { RichTextToolbarButton, RichTextShortcut } from '@wordpress/block-editor';
import { __ } from "@wordpress/i18n";

import './editor.scss';
import './style.scss'
```
:::note warn
2024.08.11追加
`useAnchor`を追加で読み込んでいます。
:::
RichTextToolbarButton用のアイコン(SVG)の設定

```jsx
const font_size_icon = (<svg id="b" data-name="layer" xmlns="http://www.w3.org/2000/svg" width="293" height="219" viewBox="0 0 293 219">
  <g id="c" data-name="container">
    <g id="d" data-name="group">
      <path id="e" data-name="path" d="m240.95,55.63h-40.35l-22.48,70.54,17.29,54.26h52l10.99,38.56h34.6l-52.05-163.37Zm-38.92,96.45s15.4-55.6,18.75-69.07c3.34,13.47,18.75,69.07,18.75,69.07h-37.49Z" stroke-width="0"/>
      <path id="f" data-name="path" d="m123.87,0h-54.1L0,219h46.39l14.73-51.7h71.42l14.73,51.7h46.39L123.87,0Zm-52.18,129.29s20.65-74.53,25.13-92.59c4.48,18.05,25.13,92.59,25.13,92.59h-50.26Z" stroke-width="0"/>
    </g>
  </g>
</svg>);
```

## registerFormatType / カスタムフォーマットタイプの登録

タグはspanを使用。クラス名なども任意のものを作成しておきます。

attributesはstyleのみを設定しました。

```jsx
registerFormatType('efc-format/fzs', {
    title: __('font size select', 'efc-block'),
    tagName: 'span',
    className:'has-font-size-picker',
    attributes: {
        'style': '',
		'id': ''
    },
    edit: MyFontSizeChange,
});
```

 `edit`プロパティにボタンなどのコンポーネント設定をしていきます。（`MyFontSizeChange`コンポーネント）

## MyFontSizeChangeコンポーネント

### 渡されるprops

```jsx
const MyFontSizeChange = (props) => {

    const { isActive, value, onChange, contentRef} = props;
/** 以下処理... */

}
```

registerFormatTypeのeditプロパティに登録するコンポーネントに渡されるpropsは

`isActive`, `value`, `onChange`, `contentRef`, `activeAttributes`などがあります。

- isActive : カスタムフォーマットが該当テキストに反映されているか
- value : 設定の値
- onChange : onChange関数は、変更された入力値を処理するために使用
- contentRef : ブロックのコンテンツが入った React 要素
- activeAttributes : 設定されたattributes 
※ただし場合により上手く値を取得できない場合ありなので今回は未使用

### 使用する値の管理 - useState / useRef の設定

各コメントに説明を入れています。

```diff_jsx
    //Popoverの開閉状態をuseStateで管理するため
    const [isPopoverOpen, setIsPopoverOpen] = useState(false);
-    //選択したテキストの情報ObjectをuseStateで管理するため
-    //useAnchorを使用するため不必要になったので削除2024.08.11
-   const [popoverPosition, setPopoverPositon] = useState(null);
    //フォントサイズを格納する変数をuseStateで管理
    const [fontSize, setFontSizeState] = useState(null);
		//前回に設定したfont sizeを格納
    const [currentFontSize, setCurrentFontSize] = useState(null);
		//前回設定した選択テキストのstart位置を管理 カーソル位置が同じブロック内だとPopoverが閉じない対策のため
    const [currentValueStart, setCurrentValueStart] = useState(value.start);
    //attributes.styleに入れるための値を格納しておく
    const useRefFont = useRef(null);
    //設定されたfont sizeを入れておくため。FontSizePickerのvalueに設定する
    const useRefSize = useRef('1em');
```

### FontSizePickerに表示する各サイズの設定

```jsx
const fontSizes = [
        {
            name: __('極小','efc-block'),
            slug: 'xsmall',
            size: "0.65em",
        },
        {
            name:  __('小','efc-block'),
            slug: 'small',
            size: "0.75em",
        },
        {
            name:  __('中','efc-block'),
            slug: 'middule',
            size: "1.25em",
        },
        {
            name:  __('大','efc-block'),
            slug: "large",
            size: "1.5em"
        },
        {
            name:  __('特大','efc-block'),
            slug: "xtralarge",
            size: "1.75em"
        },
    ];
```

相対サイズにて設定することで、親（段落）のサイズに対しての大きさとなります。

### RichTextToolbarButtonの設定

```jsx
const MyFontSizeChange = (props) => {

    const { isActive, value, onChange, contentRef} = props;
/** 以下処理... */

		/**
     * toolbar buttonの設定
     * RichTextToolbarButtonの設定
     */
    //ショートカット用の設定
    const shortcutType = 'primaryAlt';
    const shortcutCharacter = 'f';
    //ボタンのテキストを設定
    const buttonText = __('フォントサイズ変更', 'efc-block');
    //RichTextToolbarButtonの設定
    return (
        <div>
            <RichTextShortcut type={shortcutType} character={shortcutCharacter} onUse={openPopover} />
                <RichTextToolbarButton
                    icon={font_size_icon}
                    className="rubySetBtn"
                    title={buttonText}
                    onClick={ () => {
                        openPopover()
                    }}
                    isActive={ isActive }
                    shorcutType={shortcutType}
                    shorcutCharacter={shortcutCharacter}
                />
            {
                //Popover
                isPopoverOpen &&
                (<MyFontSizePoporver></MyFontSizePoporver>)
            }
        </div>
    );

}
```

`RichTextToolbar`にボタンを表示させる処理です。

`onClick`プロパティにてPopoverを表示させる関数を実行させています。

Popover自体は`MyFontSizePoporver`コンポーネントとして設定します。

`RichTextShortcut`でラップするとショートカットキーでonClickと同じ動作ができます。

### openPopover関数

`MyFontSizePoporver`コンポーネント を開くための関数

```diff_jsx
　	/**
     * RichTextToolbarButtonで追加したボタンをクリックした時の処理
     * Popoverを開く処理
     */
    const openPopover = () => {
        if (!isActive) {
            applyFontSizeFormat("1em");//初回のみ最初に設定
        }
        /**
         * useAnchorを使用するので不必要に2024.08.11
-            if (contentRef.current) {
-                //Popoverの表示位置のため選択テキストのObjectをセットしておく
-                setPopoverPositon(getSelectedRange());
-            }
        */
            setIsPopoverOpen(true);
    };
```

### MyFontSizePoporverコンポーネント

Popoverコンポーネント

```jsx
　　/**
     * Popoverコンポーネントの設定
     */
    const MyFontSizePoporver = () => {

        useEffect(() => {

            if (isActive) {
                //efc-format/fzsの設定されている値を取得
                const activeFormat = getActiveFormat(value, 'efc-format/fzs');
                //保存前（更新ボタン未クリック）と保存後でデータの持ち方が違うので、それぞれに対応するため
                const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;
                //設定してあるstyleからフォントサイズのみ取り出す
                const styledFontsize = chkAttributes.style.replace('--custom-font-size:', '');
                //font-size変更前/変更後のチェックのためのstateそれぞれに格納しておく
                setFontSizeState(styledFontsize);
                setCurrentFontSize(styledFontsize);
                //fontSizePickerのvalueに設定しているuseRefに入れる
                useRefSize.current = styledFontsize;
            }

        }, []);

        return (
            <Popover
                anchor={popoverPosition}
                placement="bottom-start"
                focusOnMount={true}
                onFocusOutside={() => {
                    // 初期値（1em)のままの場合はFormatを削除
                    resetFontSizeFormat();

                }}
            >
                <div className="efc-fontSize-popover-content" onClick={(e) => { /*popoverClick(e.target)*/}}>
                    <FontSizePicker
                        fontSizes={fontSizes}
                        value={ useRefSize.current }
                        disableCustomFontSizes={true}
                        units={["em"]}
                        //onChangeでは保存しない
                        onChange={(newFontSize) => {
                            /**
                             * 6.3.xでは既存選択ボタン以外をクリック後に既存選択ボタンを再選択してもnewFontSizeに
                             * 値が入らないので、上記popoverClick(e.target)にて対応する
                             */
                            //console.log(newFontSize);

                            useRefSize.current = newFontSize;

                            const activeFormat = getActiveFormat(value, 'efc-format/fzs');
                            const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;
                            const fontSizeSpanDOM = document.getElementById(chkAttributes.id);
                            //選択したテキストの見た目の大きさのみ変更 confirmButtonクリックでuseState走らせ値をキチンと保存
                            fontSizeSpanDOM.style.setProperty('--custom-font-size', newFontSize);

                        } }
                    />
                    <p className="popoverInfo"><span className="attention">サイズ選択後「決定」クリックで変更が反映</span></p>
                    <div className="buttonWrapper">
                        <Button
                            className="clearButton"
                            onClick={removeFontSizeFormat}
                        >{ __('クリア','efc-block') }</Button>
                        <Button
                            variant="secondary"
                            className="confirmButton"
                            onClick={() => {
                                const activeFormat = getActiveFormat(value, 'efc-format/fzs');
                                const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;
                                const fontSizeSpanDOM = document.getElementById(chkAttributes.id);
                                if (fontSizeSpanDOM) {     
                                    const styledFontsize = chkAttributes.style.replace('--custom-font-size:', '');
                                    fontSizeSpanDOM.style.setProperty('--custom-font-size', styledFontsize);
                                    if (styledFontsize === '1em') {
                                        removeFontSizeFormat();
                                    } else {
                                        closePopover();
                                    }
                                } else {
                                    closePopover();
                                }
                            }}
                        >{ __('キャンセル','efc-block') }</Button>
                        <Button
                            variant='primary'
                            className="confirmButton"
                            onClick={() => {
                                if (useRefSize.current === '1em') {
                                    alert("フォントサイズを選択してください！");
                                }
                                else if (useRefSize.current === fontSize) {
                                    closePopover();
                                } else {
                                    conformFontSize(useRefSize.current);
                                }
                            }}
                        >{ __('決定','efc-block') }</Button>
                    </div>
                </div>
            </Popover>
        );
    };
```

### **Popoverの設定**

`Popover`が開いた時に`useEffect` を使用して初期化させている

以下の箇所はpropsの`activeAttributes`で取得できそうだが保存後ページリロード時には`activeAttributes`では取得できなかった対策のため`getActiveFormat`を使って取得させている

```jsx
//efc-format/fzsの設定されている値を取得
const activeFormat = getActiveFormat(value, 'efc-format/fzs');
//保存前（更新ボタン未クリック）と保存後でデータの持ち方が違うので、それぞれに対応するため
const chkAttributes = activeFormat.unregisteredAttributes ?
activeFormat.unregisteredAttributes : activeFormat.attributes;
```

下記の箇所はPopoverで開いたが何もフォントサイズを決定させずに同じ段落の違う部分をクリックしてPopoverが閉じた時に初期値のままになるのでその場合はFormat自体を削除する設定を追加している。

```jsx
onFocusOutside={() => {
    // 初期値（1em)のままの場合はFormatを削除
		resetFontSizeFormat();
}}
```

ただし全く違う段落をクリックした際にはonFocusOutsideは感知しないのでそこのために別の処理を追加している。

### resetFontSizeFormat関数

```jsx
   /**
     * もしポップアップして何も選択せずに他のブロックをクリックした場合に
     * 初期値（1em)のままFormatが残ってしまう。
     * そのために処理で使う初期値の場合は削除する処理を行う関数
     */
    const resetFontSizeFormat = () => {
        if (getFormatAttributes().hasOwnProperty('style')) {
            if (getFormatAttributes().style === '--custom-font-size:1em' && !isPopoverOpen) {
                onChange(removeFormat(value, 'efc-format/fzs'));
            }
        }
    }
```

### getFormatAttributes関数

設定されているFormatのAttributes情報を取得

```jsx
//設定されているFormatのAttributes情報を取得
    const getFormatAttributes = () => {
        //efc-format/fzsの設定されている値を取得
        const activeFormat = getActiveFormat(value, 'efc-format/fzs');
        if (!activeFormat) {
            return {};
        }
        //保存前（更新ボタン未クリック）と保存後でデータの持ち方が違うので、それぞれに対応するため
        const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;

        return chkAttributes;
    }
```

### FontSizePickerの設定

#### onChangeプロパティ

通常はここでサイズ変更が行われた場合に設定する。
ここで`applyFormat`を実行するとPopover自体もリペイント/リフローが発生して選択ボタンアニメーションがバタバタとした動きになってしまうので仮保存の設定にしている。

またWP6.3.xの場合のFontSizePickerのonChangeにおいて、すでに値が保存されている場合にいったん別サイズのボタンをクリック後に元サイズのボタンをクリックしても値が取れない。

対応策として以下のようにラップしてpopoverClick関数内で対応する形で対応する

```jsx
<div className="efc-fontSize-popover-content" onClick={(e) => { popoverClick(e.target)}}>
 <FontSizePicker />
</div>
```

#### `popoverClick`関数
:::note warn
2024.08.11修正
エディタがiframe化しているとdocument.getElementByIdなどでのDOM操作ができない。
コールバックの引数contentRef.currentに選択しているテキストの元ブロックの情報が入っているので、それを利用するように修正する。
こちらの関数は使用せずに、その下に記載する`fontSizeChange`関数を使用するように修正しています。
:::
```jsx
/**
     * FontSicePickerオーバーライド
     * FontSizePickerにはonClickイベントがないので、divでラップしてそれにonClickを設置
     * 
     * 気がついたらonChangeで他を選択後に既存値ボタンをクリックしてもきちんと値を取得できるようになっていた
     * ので必要はないかもしれない。
     */
    const popoverClick = (target) => {
        //Popover のどこでもクリックしたら反応するのでターゲットを絞る処理を追加
        const parent = target.closest('button');
        const chkIDReg = /toggle-group-control-as-radio-group[-\d]+/;
        if (parent !== null && parent.id && parent.getAttribute("id").match(chkIDReg)) {
            const newFontSize = parent.getAttribute("data-value");

            useRefSize.current = newFontSize;

            const activeFormat = getActiveFormat(value, 'efc-format/fzs');
            const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;
            const fontSizeSpanDOM = document.getElementById(chkAttributes.id);
            //選択したテキストの見た目の大きさのみ変更 confirmButtonクリックでuseState走らせ値をキチンと保存
            fontSizeSpanDOM.style.setProperty('--custom-font-size', newFontSize);
        }
    }
```

#### `fontSizeChange`関数
applyFormatで値を決定する方法だとPopoverがチカチカとフリックしてしまうため、一時的な見た目を合わせるため、DOMでの操作をする形にしています。

```jsx
        const fontSizeChange = (newFontSize) => {
            useRefSize.current = newFontSize;

            const activeFormat = getActiveFormat(value, 'efc-format/fzs');
            const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;
            if (chkAttributes) {
                const fontSizeSpanDOM = contentRef.current.querySelector(`#${chkAttributes.id}`);
                //選択したテキストの見た目の大きさのみ変更 confirmButtonクリックでuseState走らせ値をキチンと保存
                fontSizeSpanDOM.style.setProperty('--custom-font-size', newFontSize);
            }
        }

```

## その他関数群

### getSelectedRange関数
:::note warn
2024.08.11修正
こちらもuseAnchorを使用する場合は、必要がなくなります。
:::

```diff_jsx
     /**
     * 
     * @returns 
     * 選択テキストのObjectを取得して存在したら返す
     * 存在しなかったら選択しているブロックの要素（段落）などを返す
     */
-   const getSelectedRange = () => {
-      const selection = window.getSelection();
-        const range = selection.getRangeAt(0);
-        return range ? range : contentRef.current
-    }
```

### closePopover関数

```jsx
     /** Popoverを閉じる関数 */
    const closePopover = () => {
        setIsPopoverOpen(false);
    }
```

### removeFontSizeFormat関数

クリアボタンをクリックした時に実行する関数。
全ての値なども一旦クリアさせる

```jsx
    //設定を削除
    const removeFontSizeFormat = () => {

        setFontSizeState(null);
        setCurrentFontSize(null);
        closePopover();

        return (
            onChange(removeFormat(value,'efc-format/fzs'))
        );
    };
```

## その他の処理

### resetFontSizeFormat()の実行

初期値（1em)のまま他のブロックを選択すると、Popoverが閉じてしまい初期値のままFormatが残ってしまう。他のブロックを選択した場合は変数なども引き継がないのでFormatの削除のしようがない。
対応策として次回この箇所を選択した場合に初期値のままなら一旦削除を実行させるようにする。

```jsx
     /**
     * 初期値（1em)のままの場合はFormatを削除
     * 他のブロックを選択すると、変数なども引き継がないのでFormatの削除のしようがないので
     * 次回この箇所を選択した場合に初期値のままなら一旦削除を実行させる
     */
    //
    resetFontSizeFormat();
    /**
```

### Popoverが閉じない

`Popover`が開いた状態で選択したテキストと同じブロックの違う箇所をクリックしても`Popover`が閉じない。

対策としてブロックないのカーソルの位置に変更があった場合は`Popover`を閉じるようにする

```jsx
     /**
     * カーソル位置が同じブロック内だとPopoverが閉じない対策
     * startが変わると閉じるように設定
     */
    if (currentValueStart !== value.start) {
        setCurrentValueStart(value.start);
        setFontSizeState(null);
        setCurrentFontSize(null);
        setIsPopoverOpen(false);
    }
```

### フォントサイズの確定

前回のフォントサイズと違う場合のみappleFormatを実行させる

```jsx
    //前回のフォントサイズと違う場合のみappleFormatを実行させる
    useEffect(() => {

        if (currentFontSize !== fontSize) {
            applyFontSizeFormat(fontSize);
            closePopover();
        }
        
    }, [fontSize]);
```

# コード全体

## index.js

```jsx
import { useState, useEffect, useRef } from "@wordpress/element";
import { Popover, Button,FontSizePicker } from "@wordpress/components";
import { registerFormatType, applyFormat, removeFormat, getActiveFormat, useAnchor} from '@wordpress/rich-text';
import { RichTextToolbarButton, RichTextShortcut } from '@wordpress/block-editor';
import { __ } from "@wordpress/i18n";

import './editor.scss';
import './style.scss';

const font_size_icon = (<svg id="b" data-name="layer" xmlns="http://www.w3.org/2000/svg" width="293" height="219" viewBox="0 0 293 219">
  <g id="c" data-name="container">
    <g id="d" data-name="group">
      <path id="e" data-name="path" d="m240.95,55.63h-40.35l-22.48,70.54,17.29,54.26h52l10.99,38.56h34.6l-52.05-163.37Zm-38.92,96.45s15.4-55.6,18.75-69.07c3.34,13.47,18.75,69.07,18.75,69.07h-37.49Z" stroke-width="0"/>
      <path id="f" data-name="path" d="m123.87,0h-54.1L0,219h46.39l14.73-51.7h71.42l14.73,51.7h46.39L123.87,0Zm-52.18,129.29s20.65-74.53,25.13-92.59c4.48,18.05,25.13,92.59,25.13,92.59h-50.26Z" stroke-width="0"/>
    </g>
  </g>
</svg>);


const MyFontSizeChange = (props) => {

    const { isActive, value, onChange, contentRef } = props;

    /**
     * Popover の設定
     */
    //Popoverの開閉状態をuseStateで管理するため
    const [isPopoverOpen, setIsPopoverOpen] = useState(false);
    //選択したテキストの情報ObjectをuseStateで管理するため
    //useAnchorを使用するため不必要に
    //const [popoverPosition, setPopoverPositon] = useState(null);
    //フォントサイズを格納する変数をuseStateで管理
    const [fontSize, setFontSizeState] = useState(null);
    const [currentFontSize, setCurrentFontSize] = useState(null);
    const [currentValueStart, setCurrentValueStart] = useState(value.start);
    const useRefFont = useRef(null);
    const useRefSize = useRef('1em');


    const fontSizes = [
        {
            name: __('極小','efc-block'),
            slug: 'xsmall',
            size: "0.65em",
        },
        {
            name:  __('小','efc-block'),
            slug: 'small',
            size: "0.75em",
        },
        {
            name:  __('中','efc-block'),
            slug: 'middule',
            size: "1.25em",
        },
        {
            name:  __('大','efc-block'),
            slug: "large",
            size: "1.5em"
        },
        {
            name:  __('特大','efc-block'),
            slug: "xtralarge",
            size: "1.75em"
        },
    ];


    /**
     * 
     * @returns 
     * 選択テキストのObjectを取得して存在したら返す
     * 存在しなかったら選択しているブロックの要素（段落）などを返す
     *
     * useAnchorをしようするので不必要に
    const getSelectedRange = () => {
        const selection = window.getSelection();
        const range = selection.getRangeAt(0);
        return range ? range : contentRef.current
    }
    */
    
    /**
     * useAnchorの設定
     * useAnchorのsettingsにFormatTypeの情報+isActiveを渡すことで
     * 正確な位置を取得してくれる
   */
    const settings = {
	name:'efc-format/fzs',
	title: __('font size select', 'efc-block'),
	tagName: 'span',
	className: 'has-font-size-picker',
	attributes: {
		style: '',
	},
	edit: MyFontSizeChange,
    };
    const popoverAnchor = useAnchor({
        editableContentElement: contentRef.current,
        settings: { ...settings,isActive }
    });
    /**
     * RichTextToolbarButtonで追加したボタンをクリックした時の処理
     * Popoverを開く処理
     */
    const openPopover = () => {
        if (!isActive) {
            applyFontSizeFormat("1em");//初回のみ最初に設定
        }
        /**
         * useAnchorを使用するので不必要に
        if (contentRef.current) {
            //Popoverの表示位置のため選択テキストのObjectをセットしておく
            //setPopoverPositon(getSelectedRange());
        }
        */
        setIsPopoverOpen(true);
    };
    /** Popoverを閉じる関数 */
    const closePopover = () => {
        setIsPopoverOpen(false);
    }

    /**
     * Font Size の変更するための設定
     * フォントサイズを反映させる 
    */
    const applyFontSizeFormat = (fontSize) => {
        
        setCurrentFontSize(fontSize);
        useRefFont.current = `--custom-font-size:${fontSize}`;
        const myId = `cfz-${crypto.randomUUID()}`;

        onChange(

            applyFormat(value, {
                type: 'efc-format/fzs',
                attributes: {
                    style: useRefFont.current,
                    id: myId,
                }
            })
        )
    };
    //設定を削除
    const removeFontSizeFormat = () => {

        setFontSizeState(null);
        setCurrentFontSize(null);

        closePopover();

        return (
            onChange(removeFormat(value,'efc-format/fzs'))
        );
    };
    //前回のフォントサイズと違う場合のみappleFormatを実行させる
    useEffect(() => {

        if (currentFontSize !== fontSize) {
            applyFontSizeFormat(fontSize);
            closePopover();
        }
        
    }, [fontSize]);

    //Confirm Font Size
    const conformFontSize = (newFontSize) => {
        setFontSizeState(newFontSize);
    }
    
    /**
     * カーソル位置が同じブロック内だとPopoverが閉じない対策
     * startが変わると閉じるように設定
     */
    if (currentValueStart !== value.start) {
        setCurrentValueStart(value.start);
        setFontSizeState(null);
        setCurrentFontSize(null);
        setIsPopoverOpen(false);
    }


    /**------- Copmonentの設定---------- */
    
    /**
     * Popoverコンポーネントの設定
     */
    const MyFontSizePoporver = () => {

        useEffect(() => {

            if (isActive) {
                
                const activeFormat = getActiveFormat(value, 'efc-format/fzs');
                const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;

                const styledFontsize = chkAttributes.style.replace('--custom-font-size:', '');
                setFontSizeState(styledFontsize);
                setCurrentFontSize(styledFontsize);

                useRefSize.current = styledFontsize;
            }

        },[]);
        
        /**
         * FontSicePickerオーバーライド
         * FontSizePickerにはonClickイベントがないので、divでラップしてそれにonClickを設置
         * 
         * FontSicePickerのonChangeがcurrentの場合でも一旦ほかに変更したら動作することを確認
        const popoverClick = (target) => {
            //Popover のどこでもクリックしたら反応するのでターゲットを絞る処理を追加
            const parent = target.closest('button');
            const chkIDReg = /toggle-group-control-as-radio-group[-\d]+/;
            if (parent !== null && parent.id && parent.getAttribute("id").match(chkIDReg)) {
                const newFontSize = parent.getAttribute("data-value");

                useRefSize.current = newFontSize;

                const activeFormat = getActiveFormat(value, 'efc-format/fzs');
                const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;
                const fontSizeSpanDOM = document.getElementById(chkAttributes.id);
                //選択したテキストの見た目の大きさのみ変更 confirmButtonクリックでuseState走らせ値をキチンと保存
                fontSizeSpanDOM.style.setProperty('--custom-font-size', newFontSize);
            }
        }
        */
        
        const fontSizeChange = (newFontSize) => {
            useRefSize.current = newFontSize;

            const activeFormat = getActiveFormat(value, 'efc-format/fzs');
            const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;
            if (chkAttributes) {
                const fontSizeSpanDOM = contentRef.current.querySelector(`#${chkAttributes.id}`);
                //選択したテキストの見た目の大きさのみ変更 confirmButtonクリックでuseState走らせ値をキチンと保存
                fontSizeSpanDOM.style.setProperty('--custom-font-size', newFontSize);
            }
        }

        return (
            <Popover
                anchor={popoverAnchor}
                placement="bottom-start"
                focusOnMount={false}
            >
                <div className="efc-fontSize-popover-content">
                    <FontSizePicker
                        fontSizes={fontSizes}
                        value={ useRefSize.current }
                        disableCustomFontSizes={true}
                        units={["em"]}
                        onChange={(newFontSize) => {
                            fontSizeChange(newFontSize);
                        } }
                    />
                    <p className="popoverInfo"><span className="attention">サイズ選択後「決定」クリックで変更が反映</span></p>
                    <div className="buttonWrapper">
                        <Button
                            className="clearButton"
                            onClick={removeFontSizeFormat}
                        >{ __('クリア','efc-block') }</Button>
                        <Button
                            variant="secondary"
                            className="confirmButton"
                            onClick={() => {
                                const activeFormat = getActiveFormat(value, 'efc-format/fzs');
                                //選択して反映させたFormatの情報(attributes)を取得
                                const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;

                                //const fontSizeSpanDOM = document.getElementById(chkAttributes.id);
                                //contentRef.currentにはFormatを反映させたテキストのritch-textのObjectが入っているのでそこからFormatを適応させたElementを取得
                                const fontSizeSpanDOM = contentRef.current.querySelector(`#${chkAttributes.id}`);
                                const styledFontsize = chkAttributes.style.replace('--custom-font-size:', '');
                                fontSizeSpanDOM.style.setProperty('--custom-font-size', styledFontsize);
                                if (styledFontsize === '1em') {
                                    removeFontSizeFormat();
                                } else {
                                    closePopover();
                                }
                            }}
                        >{ __('キャンセル','efc-block') }</Button>
                        <Button
                            variant='primary'
                            className="confirmButton"
                            onClick={() => {
                                if (useRefSize.current === '1em') {
                                    alert("フォントサイズを選択してください！");
                                }
                                else if (useRefSize.current === fontSize) {
                                    closePopover();
                                } else {
                                    conformFontSize(useRefSize.current);
                                }
                            }}
                        >{ __('決定','efc-block') }</Button>
                    </div>
                </div>
            </Popover>
        );
    };
    /**
     * toolbar buttonの設定
     * RichTextToolbarButtonの設定
     */
    //ショートカット用の設定
    const shortcutType = 'primaryAlt';
    const shortcutCharacter = 'f';
    //ボタンのテキストを設定
    const buttonText = __('フォントサイズ変更', 'efc-block');
    //RichTextToolbarButtonの設定
    return (
        <div>
            <RichTextShortcut type={shortcutType} character={shortcutCharacter} onUse={openPopover} />
                <RichTextToolbarButton
                    icon={font_size_icon}
                    className="rubySetBtn"
                    title={buttonText}
                    onClick={ () => {
                        openPopover();
                    }}
                    isActive={ isActive }
                    shorcutType={shortcutType}
                    shorcutCharacter={shortcutCharacter}
                />
            {
                //Popover
                isPopoverOpen &&
                (<MyFontSizePoporver></MyFontSizePoporver>)
            }
        </div>
    );
}



registerFormatType('efc-format/fzs', {
    title: __('font size select', 'efc-block'),
    tagName: 'span',
    className:'has-font-size-picker',
    attributes: {
        'style': '',
        'id':''
    },
    edit: MyFontSizeChange,
});
```

## スタイルシート

## editor.scss

```scss
.has-font-size-picker {
    --custom-font-size:1em;
    font-size: var(--custom-font-size);
}
.efc-fontSize-popover-content{
    padding: 12px;
    min-width: 300px;
}
.popoverInfo {
    margin-top: -12px;
    font-size: 0.75rem;
    color:#666;
}
.buttonWrapper {
    display: flex;
    gap: 8px;
}
.attention {
    color:#3858e9;
}
```

## style.scss

```scss
.has-font-size-picker {
    --custom-font-size:1em;
    font-size: var(--custom-font-size);
}
```
# WordPress6.6以前の環境に合わせるためのPolyfillの追加
WordPress6.6での使用のためにBuildは、@wordpress/scriptsのバージョン28.x.xで行う必要がありました。
単純のアップデートしてbuildを仕直しすれば6.6でのOKなのですが、そうすると6.5などそれ以前のWordPress環境でエラーになります。
対処法としては、下記のURLに記載されているPolyfillを読み込ませておくと大丈夫でした。

https://github.com/WordPress/gutenberg/issues/62202#issuecomment-2156796649

まずは　任意のディレクトリを作成してそこに以下の内容でpackage.jsonを作成します。

```json
{
  "name": "runtime",
  "version": "1.0.0",
  "main": "webpack.conf.js",
  "scripts": {
    "build:runtime": "webpack --config webpack.config.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "react": "^18.3.1",
    "webpack": "^5.93.0",
    "webpack-cli": "^5.1.4"
  }
}

```
そしてインストールします。
```bash
$ npm i
```
同じ階層にwebpack.config.jsファイルを以下の記述で配置
```js
const path = require('path');

module.exports = {
  entry: {
    'react-jsx-runtime': 'react/jsx-runtime',
  },
  output: {
    path: path.resolve(__dirname, '../assets/js'),
    filename: 'react-jsx-runtime.js',
    library: {
      name: 'ReactJSXRuntime',
      type: 'window',
    },
  },
  externals: {
    react: 'React',
  },
};
```
そして　buildを実行
```bash
$ npm run build:runtime
```
そうすると`assets/js`ディレクトリに`react-jsx-runtime.js`ファイルが作成されます。
PHPにて作成した`react-jsx-runtime.js`ファイルを読み込みます。

```php

 /**
  *  WordPress6.6以前用のPolyfill読み込み
  */
 function my_plugin_block_editor_scripts() {
    // JSXランタイムのポリフィルスクリプトを登録
    wp_register_script(
        'react-jsx-runtime',
        plugins_url('assets/js/react-jsx-runtime.js', __FILE__),
        [ 'react' ],
        '18.3.0',
        true
    );

    wp_enqueue_script('react-jsx-runtime');
}
add_action('enqueue_block_editor_assets', 'my_plugin_block_editor_scripts');
```
これで6.6以前のバージョンでも大丈夫になります。


# 雑感
:::note info
今回はどうしてもFontSizePickerをPopoverにて使いたかったので割とトリッキーな手法になってしまっています。
もしかして`FontSizePicker`は`InspectorControls`での使用を想定しているコンポーネントなのかもしれません。
:::
:::note info
Popoveコンポーネント、もしくはFontSizePickerなどは`registerFormatType`の`edit`コールバック関数内にて定義をしていましたが、もしかしてuseMemoなどを使用して外部で定義をすると各Sizeボタンクリックで、即`onChange(applyFormat())`を実行しても、フリッカーがおきないかもしれないので、時間を見てテストしたいと思っています。
:::
:::note info
 FontSizePickerのonChangeにて、保存されている値のFont Sizeと同じボタンクリックで6.3.xでは値が取れなかったのが6.4.xでは値が取れるようになった件、何か知っている方がいましたらコメント残していただけるとありがたいです。
:::
# 参考サイト/ページ
https://ja.wordpress.org/team/handbook/block-editor/how-to-guides/format-api/

https://developer.wordpress.org/block-editor/reference-guides/packages/packages-rich-text/

[FontSizePicker GitHub](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/font-size-picker/README.md)

[popover GitHub](https://github.com/WordPress/gutenberg/tree/trunk/packages/components/src/popover)

[Format Library: Try to fix highlight popover jumping【GitHub】](https://github.com/WordPress/gutenberg/pull/54736)