【Gutenberg】書式ツールバーFormat APIを使いテキストにルビを追加する機能を作ってみた！

# 概要

https://ja.wordpress.org/team/handbook/block-editor/how-to-guides/format-api/

Wordpressにはルビを振る機能がないので書式ツールバー - Format APIにてルビを挿入できるようなカスタムフォーマットを作成

![add_ruby2023-09-28-17.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34746/6271d113-f415-4736-d3bb-a38e85c45b88.gif)

# 要件定義

1. 選択したテキスト部分にルビを入れるようにする
2. ルビを入力するフォームはPopoverにて選択テキストの下に表示されるようにする

# 仕様

1. `RichTextToolbarButton`にカスタムボタン -  “ルビを追加”ボタンを追加しそれをクリックするとPopoverにてルビの入力欄が表示する
2. Popoverには入力欄と”ルビを追加”ボタンのみとする
3. 一度ルビを設定した箇所の編集機能は追加せずに、該当箇所を選択した場合には`RichTextToolbarButton`の表示は”ルビを削除”ボタンと表示を変えて、一旦削除させるようにする
3. 入力されるテキストはサニタイズをしておく

:::note warn
2024.08.13修正
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化などが行われてきているので、その辺りの修正を追加しました。
:::



# 実装

実装は@wordpress/scriptを使用して作成します。

## コード全体

```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,
});
```

# 各コードの解説

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

```diff_jsx
import { Fragment, useState} from "@wordpress/element";
import { Popover, Button, TextControl } from "@wordpress/components";
import { registerFormatType, applyFormat, removeFormat, getActiveFormat, insert, replace,
+ useAnchor
} from '@wordpress/rich-text';
import { RichTextToolbarButton, RichTextShortcut} from '@wordpress/block-editor';
import { __, sprintf, _n } from "@wordpress/i18n";
/**
 * HTMLサニタイズをしてくれるnode moduleを読み込む
 * 
 */
import DOMPurify from 'dompurify';
import './editor.scss';
```
https://developer.wordpress.org/block-editor/reference-guides/packages/packages-rich-text/


ルビ用のテキストを挿入したりするので、`@wordpress/rich-text`の`insert`や`replace`を使用します。

https://www.npmjs.com/package/dompurify

ルビテキストをユーザーが入力するので、入力の値をサニタイズなどを行なってから保存したいため上記のモジュールをインストールしています。

## FormatTypeの登録

`registerFormatType`で`FormatType` を登録します。今回はルビなので、rubyタグとrtタグの二つのタグを使うので、それぞれ別の`FormatType`として登録します。

```jsx

/**
 * rtタグ用のFormat Typeを登録
 */
registerFormatType( 'efc-format/rt', {

  title: __( 'Ruby Character', 'efc-block' ),

  tagName: 'rt',

  className: null,

  edit( {isActive, value, onChange} ) {
    return <Fragment></Fragment>
  }

} );
 /**
  * rubyタグ用のFormat Typeを登録
  */
registerFormatType( 'efc-format/ruby', {
    title:  __( 'Ruby Wrapper', 'efc-block' ),
    tagName: 'ruby',
    className: null,
    attributes: {
        'data-rt': '',//ルビのテキストをdate-rt属性として保存しておく
    },
    edit: MyCustomButton,//テキストにFotmatを適応するためのボタン設定
} );
```

`ruby`タグ用の`FormatType`には`edit`プロパティにボタンやPopoverなどを設定したコンポーネント`MyCustomButton`を登録。
`attributes`には`’data-rt’`として`rt`タグに入れるルビテキストを格納するように設定しておく。

`RichTextToolbarButton`は`ruby`タグ用の`FormatType`にのみあれば良いので、`rt`タグ用の`FormatType`のeditプロパティには空のものを入れておきます。

## MyCustomButtonコンポーネントの設定

`MyCustomButton`コンポーネントには、`RichTextToolbarButton`コンポーネントや`Popover`での入力欄、データの反映/保存などを設定します。

`MyCustomButton`コンポーネントに渡させるpropsの内`isActive`, `value`, `onChange`, `contentRef`を使用します。

```jsx
const MyCustomButton = (props) => {
    /** propsからisActive、value、onChange を取り出して使う */
    const { isActive, value, onChange, contentRef } = props;
....
}
```

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

## 変数/useStateなどの設定

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

    /** propsからisActive、value、onChange を取り出して使う */
    const { isActive, value, onChange, contentRef } = props;

    //入力されたルビテキストを一時的に格納する変数
    let rubyText;

    //Popoverの開閉状態をuseStateで管理するため
    const [isPopoverOpen, setIsPopoverOpen] = useState(false);

    //これはPopoverのanchorに使用するためのもので
    //選択したテキストの情報ObjectをuseStateで管理するため
    const [popoverPosition, setPopoverPositon] = useState(null);

....
}
```

`rubyText`変数は一時的に使用するため通常の変数として宣言しています。`useRef`の設定の方がReact的に良いのですが、`useRef`での管理では更新のタイミングなどで問題が出るケースがあったので通常の変数にしています。

`popoverPosition`は`Popover`の表示位置を設定する`anchor`プロパティに入れるために選択したテキストのObject情報を入れておくようにします。

## RichTextToolbarButtonコンポーネントの設定

```jsx
//ショートカット用の設定
    const shortcutType = 'primaryAlt';
    const shortcutCharacter = 'r';
    //ボタンのテキストを設定
    const buttonText = isActive ? __('ルビを削除', 'efc-block') : __('ルビを追加', 'efc-block');
    //RichTextToolbarButtonの設定
    return (
        <div>
            <RichTextShortcut type={shortcutType} character={shortcutCharacter} onUse={openPopover} />
                <RichTextToolbarButton
                    icon="edit"
                    className="rubySetBtn"
                    title={buttonText}
                    onClick={ async () => {
                        openPopover();
                    }}
                    shorcutType={shortcutType}
                    shorcutCharacter={shortcutCharacter}
                />
            {
                //Popover
                isPopoverOpen && 
                (<MyPoporver></MyPoporver>)
            }
        </div>
    );
```

`RichTextToolbarButton`コンポーネントとショートカット用の`RichTextShortcut`コンポーネントでruby用のツールバーボタンの設定をします。

`isActive`でテキスト文字の表示を振り分けています。

`onClick`プロパティに`openPopver`関数を渡してクリックすると`Popover`が表示されます。

## openPopover関数

`RichTextToolbarButton`の`onClick`プロパティに登録している関数で、ルビ登録用の`Popover`を表示したり、ルビを削除したりする機能を入れています。

```jsx
//RichTextToolbarButtonで追加したボタンクリックした時の処理
    const openPopover =  () => {
    
        if (isActive) {
            //すでにルビが振ったあるテキストが選択されていたら削除関数を実行
            removeRuby()
        } else {

            if (contentRef.current) {
                //選択テキストのObjectをセットしておく
                setPopoverPositon(getSelectedRange());
            }
            //Popoverを開らかせるフラグをtrueに
            setIsPopoverOpen(true);

        }
    }
```

## removeRuby関数

ルビを削除する関数です。
すでにルビが振ったあるテキストが選択されていたら削除関数を実行する様にしています。
ルビの場合ルビテキスト（ひらがな/カタカナ）の文字分ルビがない場合より長くなっています。その分をMyCustomButtonコンポーネントに渡されるvalueに格納されているテキストから削除しなければなりません。しかしvalueは読み取り専用となるので、一旦別の変数に格納してそれを使いテキストのルビ部分を`replace`メソッドで削除する処理を行なっています。

```jsx
const removeRuby = () => {

        //valueが読み取り専用となるので一旦currentValueへ入れて加工していく
        let currentValue = value;

        //状態を取得して設定しているdata-rubyからルビテキストを取得しておく
        const activeFormat = getActiveFormat(currentValue, 'efc-format/ruby');

        /**
         * attributesはformatTypeを設定直後の状態ではatteributesで取得となるが
         * ページをリロード or 遷移した後ではunregisteredAttributesでの取得となるのでその振りわけ
        */
        const chkAttributes = activeFormat.unregisteredAttributes ? activeFormat.unregisteredAttributes : activeFormat.attributes;
        const rubyText = chkAttributes['data-ruby'];

        //選択テキストの開始/終了の位置を変数に入れておく
        const rubyEnd   = currentValue.end;
        const rubyStart = currentValue.start; 

        //まずはrubyタグのformatTypeの適応を解除
        currentValue = removeFormat(currentValue, 'efc-format/ruby', rubyStart, rubyEnd);

        //data-rubyから取得したテキストの開始位置を調べてrtStart変数に格納
        const rtStart = currentValue.text.indexOf(rubyText);
        
        //テキスト文字長から終端を調べてrtEndに格納
        const rtEnd = rtStart + rubyText.length;
        
        //上記設定の開始/終端を使用してrtタグのformatTypeの適応を解除
        currentValue = removeFormat(currentValue, 'efc-format/rt', rtStart, rtEnd);

        //replaceを使ってrubyTextで削除
        currentValue = replace(currentValue, rubyText, '');
        
        //整形した値をonChangeで返す
        return onChange( currentValue );

    }
```

## MyPoporverコンポーネントの設定

[Popover | Block Editor Handbook | WordPress Developer Resources](https://developer.wordpress.org/block-editor/reference-guides/components/popover/)

Popoverは上記のドキュメントを参考に設定します。
表示する位置に関しては`anchor`プロパティと`placement`プロパティで設定します。

```jsx
 		/**
     * 
     * @returns　Component
     * Popoverコンポーネント
     */
    const MyPoporver = () => {

        return (
        <Popover
                onClose={closePopover}
                anchor={popoverPosition}
                placement="bottom-start"
        >
            <div className="efc-addRuby-popover-content">
                <TextControl
                    label="ルビテキストを入力してください"
                    className="input-rubyText"
                    value={rubyText}
                    onChange={onInputChange} // テキスト入力の変更時にrubyText変数を更新
                />
                <Button variant="primary" className="button-addRuby" onClick={onConfirm}>{__('ルビを追加', 'efc-block')}</Button>
            </div>
        </Popover>)
    }
```

> **`anchor`:`Element | VirtualElement | null`**
ポップオーバーがアンカーとして使用する要素。これは `Element` か、あるいは `VirtualElement` - `getBoundingClientRect()` と `ownerDocument` プロパティを定義したオブジェクト - になります。
> 
> 
> 要素が変更されたときに確実にリアクティブに更新されるように、要素はプレーンなrefではなくstateに格納する必要があります。
> 

https://developer.wordpress.org/block-editor/reference-guides/components/popover/#anchor-element-virtualelement-null

とあるので`anchor`プロパティには`getBoundingClientRect()が`定義されている`Object`を登録する。`window.getSelection()`にて選択したテキスト部分に関しての同等の`Object`を取得できるので、前述の`openPopover`関数内で、後述の`getSelectedRange`関数を使用して`popoverPosition`に代入します。

### TextControlコンポーネント

`Popover`内には`TextControl`コンポーネントを配置して、こちらにルビてキストを入力できるようにしています。

`onChange`プロパティにて`onInputChange`関数を走らせてルビの内容を`rubeText`変数に格納させています。

### Buttonコンポーネント

`onClick`プロパティにて入力したルビを適応するための`onConfirm`関数を実行します。

## onInputChange関数

DOMPurifyモジュールを使ってサイニタイズしたものを変数に入れています。

```jsx
  	/**
     * @param {text} newRubyText ルビテキスト
     * 処理でサニタイズなどをしている
     */
    const onInputChange = (newRubyText) => {
        //翻訳機能を使ってサニタイズでは漏れてしまう悪意のある文字列を変換するなど
        rubyText = sprintf(_n('%s', 'efc-blocks'), newRubyText);
        //HTMLサニタイズ
        rubyText = DOMPurify.sanitize(rubyText);
    };
```

`rubyText = sprintf(_n('%s', 'efc-blocks'), newRubyText);`の箇所は翻訳機能を使いサニタイズでは漏れてしまう悪意のある文字列を置き換えるなどができます。

※ここの箇所は必要なければ省いてしまっても動作自体は問題ありません。
翻訳ファイルの作成/登録の方法などは手前味噌になりますが、

https://qiita.com/soundweaver/items/ed65fc4ac2fca753b36b

こちらなどが参考になるかと思います。

## getSelectedRange関数
:::note alert
2024.08.13修正
こちらは、公式のuseAnchorを使用する形で対応できるので、不必要になります。
代わりに次項のuseAnchorを使用したpopoverAnchorを追加しました。
:::

`Popove`rの`anchor`プロパティに選択テキストのObject情報を渡すためにObject情報を取得する関数。

`window.getSelection()`にて選択したテキストの位置情報が取得できる。取得できなかった場合は、現在選択されているブロックの情報を渡しておく。

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

    }
```
## useAnchorモジュールを使用して選択したテキストの位置を取得する

```javascript
    /**
     * useAnchorの設定
     * useAnchorのsettingsにFormatTypeの情報+isActiveを渡すことで
     * 正確な位置を取得してくれる
   */
    const settings = {
        name: 'efc-format/ruby',
        title: 'ruby',
        className: null,
        attributes: {
            'data-rt': ''
        },
        edit: MyCustomButton
    };
    const popoverAnchor = useAnchor({
        editableContentElement: contentRef.current,
        settings: { ...settings, isActive }
    });

```

## onConfirm関数

`onConfirm`関数にて後述のルビの`FormatType`を登録する`onChangeCallback`関数とPopoverを閉じる関数を実行する。

```jsx
    /**
     * 入力完了の処理
     */
    const onConfirm = () => {
        //Popoveを閉じる
        closePopover();
        //ルビを適応させる関数を実行
        onChangeCallback();
    }
```

## onChangeCallback関数

```jsx
const onChangeCallback = () => {

        let ruby,currentValue;
        ruby = rubyText;
        /**
         * window.promptなどで入力欄を設定している時は選択したテキストのfocusや選択状況は解除されないが
         * Popoverで入力欄を作った場合にfocusがそちらに移ってしまう。
         * そうするとvalueが読み取り専用になってしまうのでcurrentValue変数に入れて加工する
         */
        currentValue = value;
        if (ruby === '') return;
        
        // ルビの親文字の位置を調べる変数に入れておく
        const rubyEnd   = currentValue.end;
        const rubyStart = currentValue.start;

        // currentValue(value)Objectのtextにrubyテキストを追加する
        currentValue = insert(currentValue, ruby, rubyEnd);
        //insertでテキストを追加するとstart/endが更新されるので再度代入する
        currentValue.start = rubyStart;
        currentValue.end = rubyEnd + ruby.length;
        
        // rubyタグを適用。
        currentValue = applyFormat(currentValue,
            {
                type: 'efc-format/ruby',
                attributes: {
                    "data-ruby": ruby,//data-ruby属性にルビテキストを保存しておく / 削除する際に必要
                }
            },
            rubyStart,
            rubyEnd + ruby.length
        );
        //rtタグを適応
        currentValue = applyFormat( currentValue, {
        type: 'efc-format/rt'
        }, rubyEnd, rubyEnd + ruby.length );
        
        //整形した値をonChangeで返す
        return onChange(currentValue);
        
    }
```

## closePopover関数

```jsx
    //Popoverを閉じる関数
    const closePopover = () => {
        setIsPopoverOpen(false);
    }
```
# 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以前のバージョンでも大丈夫になります。


# 参考URL

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

https://www.npmjs.com/package/dompurify

https://github.com/WordPress/gutenberg/blob/trunk/docs/how-to-guides/format-api.md

https://developer.wordpress.org/block-editor/reference-guides/components/popover/