【microCMS】拡張フィールドでタブ切り替え機能付き入力フィールドを実装してみた

## 概要  
microCMSを利用して制作しているWebサイトで多言語対応をすることになり、一つのコンテンツの中で日本語、英語など複数言語のテキストをセットで入力できる機能が欲しくなった。

カスタムフィールドなど既存機能で対応すると、フィールドの高さが出たり入力欄が狭くなったりして好みではなかったので、拡張フィールド機能でタブ切り替えできる入力フィールドを作成した。  

![multi_field_cap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/367886/540be717-4f8f-991b-4f5e-1331e8514e98.png)

全体のソースコードは後半に貼ってます。
拡張フィールド実装のご参考までに。

## 管理画面・レスポンスサンプル  
### 管理画面
![multi_field_cap.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/367886/f8e4934d-026d-1f6a-8d6f-c90dc3ef048f.gif)

### APIレスポンス
```
{
    "id": "〜〜〜",
    "createdAt": "〜〜〜",
    〜〜〜
    "multi_field": {
        "en": "<p>english text<br>abc<br>def</p>",
        "de": "<p>deutscher Text</p>",
        "ja": "<p>日本語テキスト</p>"
    }
    〜〜〜
}
```

## 各機能について
### タブ切り替え機能はGoogleのMaterial Webフレームワークを利用
簡単に見た目が整ったUIを実装できてたすかる。
各UIの実装方法は[GitHubのdocs](https://github.com/material-components/material-web/tree/main/docs/components)を参照。

### エディタはQuillを利用
オープンソースで機能も充実しているのでたすかる。
WYSIWYG機能がついているので、必要に応じて装飾機能なども追加できる。
リンク機能を使う場合、「target="_blank"」の制御には別途設定が必要。（[参照](https://github.com/slab/quill/issues/1139)）
  
## 拡張フィールド実装のメモ
### 管理画面からコンテンツを受け取る
```
window.addEventListener('message', (e) => {
  if (
    e.isTrusted === true &&
    e.data.action === 'MICROCMS_GET_DEFAULT_DATA'
  ) {
    microCMSOrigin = e.origin
    frameID = e.data.id
    initialContents = e.data.message.data

    // 〜〜〜
    // エディタに初期値を設定するなどの処理
    // 〜〜〜
    }
})
```
`e.origin`：管理画面のorigin。コンテンツを送信するときに利用するので取っておく。
`e.data.id`：iframeのID。コンテンツを送信するときに利用するので取っておく。
`e.data.message`：microCMS側で設定・保持しているフィールド情報やコンテンツ情報が格納されている。
<details><summary>e.data.messageの例</summary>

```
{
    "description": "複数のテキストエリアをタブで切り替えて入力するフィールドです。",
    "id": "multiTextarea",
    "title": "タブ切り替えフィールド",
    "data": {
        "en": "<p>english text</p>",
        "de": "<p>deutscher Text</p>",
        "ja": "<p>日本語テキスト</p>"
    }
}
```

</details>

### 管理画面にコンテンツを送信する
```
window.parent.postMessage(
  {
    id: [iframeのID],
    action: 'MICROCMS_POST_DATA',
    message: {
      id: 'multiTextarea',
      title: 'タブ切り替えフィールド',
      description: '複数のテキストエリアをタブで切り替えて入力するフィールドです。',
      data: {
        [登録したいコンテンツ]
      }
    }
  },
  [管理画面のorigin]
);
```
`message`の内容が管理画面に登録される。
APIで取得できるのは`message.data`の中身。


### 管理画面上の拡張フィールドのサイズを設定する
```
window.parent.postMessage(
  {
    id: [iframeのID],
    action: 'MICROCMS_UPDATE_STYLE',
    message: {
      height: [設定したいフィールドの高さ],
      width: '100%'
    }
  },
  [管理画面のorigin]
);
```
高さはギリギリで設定するとスクロールバーが出ちゃうので、10px程度バッファを持たせるとよさそう。

## 実装したHTML
拡張フィールド実装のご参考までに。
（カスタムしながら使ってるので、冗長なのは許して...）
**※こちらのHTML・スクリプトの利用で不具合等が発生しても責任は取りません。利用する際は十分なデバッグ・バックアップの上でご利用ください。**

<details><summary>（クリックで展開）HTMLソースコード</summary>

```html:index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">

  <!-- Quill >>> -->
  <link href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css" rel="stylesheet" />
  <!-- >>> Quill -->

  <!-- Material Web >>> -->
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
  <script type="importmap">
    {
      "imports": {
        "@material/web/": "https://esm.run/@material/web/"
      }
    }
  </script>
  <script type="module">
    import '@material/web/all.js';
    import { styles as typescaleStyles } from '@material/web/typography/md-typescale-styles.js';
    document.adoptedStyleSheets.push(typescaleStyles.styleSheet);
  </script>
  <!-- >>> Material Web -->

  <style>
    body {
      margin: 0;
    }

    .tab--wrap {
      width: 100%;
    }

    .panel__container {
      width: 100%;
    }
  </style>
</head>

<body>
  <form id="multiInputFieldWrap">
    <md-tabs class="tab--wrap">
      <md-secondary-tab id="ja-tab" class="tab__content" aria-controls="ja-text-panel">日本語</md-secondary-tab>
      <md-secondary-tab id="en-tab" class="tab__content" aria-controls="en-text-panel">英語</md-secondary-tab>
      <md-secondary-tab id="de-tab" class="tab__content" aria-controls="de-text-panel">ドイツ語</md-secondary-tab>
    </md-tabs>

    <div id="ja-text-panel" class="panel__container" role="tabpanel" aria-labelledby="ja-tab">
      <div id="ja-text"></div>
    </div>
    <div id="en-text-panel" class="panel__container" role="tabpanel" aria-labelledby="en-tab">
      <div id="en-text"></div>
    </div>
    <div id="de-text-panel" class="panel__container" role="tabpanel" aria-labelledby="de-tab">
      <div id="de-text"></div>
    </div>
  </form>

  <!-- Quill >>> -->
  <script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>
  <!-- >>> Quill -->

  <script>
    // ================================
    // 共通変数
    // ================================
    const tabIDAry = ['ja', 'en', 'de']
    let tabInputValueObj = {}
    tabIDAry.forEach((id) => {
      tabInputValueObj[id] = ''
    })

    // ================================
    // Quill設定
    // ================================
    let quillObj = {}

    // quillエディタのインスタンスを生成、DOMに反映する
    tabIDAry.forEach((id) => {
      quillObj[id] = new Quill(`#${id}-text`, {
        theme: 'snow',
        modules: {
          toolbar: false
        },
        placeholder: 'テキストを入力してください'
      });

      quillObj[id].on('text-change', (delta, oldDelta, source) => {
        updateData()
        updateEditorSize()
      })
    })

    document.getElementById('en-text-panel').setAttribute('hidden', true)
    document.getElementById('de-text-panel').setAttribute('hidden', true)

    // ================================
    // タブ切り替え機能
    // ================================
    const tabElmAry = document.querySelectorAll('.tab__content')
    const panelElmAry = document.querySelectorAll('.panel__container')
    tabElmAry.forEach((elm) => {
      elm.addEventListener('click', () => {
        panelElmAry.forEach((elm) => {
          elm.setAttribute('hidden', true)
        })
        const panelID = elm.getAttribute('aria-controls')
        const targetPanelElm = document.getElementById(panelID)
        targetPanelElm.removeAttribute('hidden')
        updateEditorSize()
      })
    })

    // ================================
    // microCMSとの通信機能
    // ================================
    let microCMSOrigin = ''
    let frameID = ''
    let initialContents = ''

    // 初期情報の取得・流し込みと、拡張フィールドサイズの設定
    window.addEventListener('message', (e) => {
      if (
        e.isTrusted === true &&
        e.data.action === 'MICROCMS_GET_DEFAULT_DATA'
      ) {
        microCMSOrigin = e.origin
        frameID = e.data.id
        initialContents = e.data.message

        // テキストエリアの初期値を設定する
        if (initialContents) {
          // 初期値が存在する場合、エディタに反映する
          tabIDAry.forEach((id) => {
            const currentValue = initialContents.data[id]
            if (currentValue) {
              tabInputValueObj[id] = currentValue
              quillObj[id].clipboard.dangerouslyPasteHTML(currentValue)
            }
          })
        } else {
          // 初期値が存在しない場合は、内容が空の状態でPOSTする
          window.parent.postMessage(
            {
              id: frameID,
              action: 'MICROCMS_POST_DATA',
              message: {
                id: 'multiTextarea',
                title: 'タブ切り替えフィールド',
                description: '複数のテキストエリアをタブで切り替えて入力するフィールドです。',
                data: tabInputValueObj
              }
            },
            microCMSOrigin
          );
        }
        updateEditorSize()
      }
    });

    /** 入力された内容をHTML形式にフォーマットし、microCMSに送信する */
    function updateData() {
      tabIDAry.forEach((id) => {
        const resultRawHTML = quillObj[id].getSemanticHTML()
        const resultFormatHTML = resultRawHTML.replace(/\<\/p\>\<p\>/g, '<br>')
        tabInputValueObj[id] = resultFormatHTML
      })

      window.parent.postMessage(
        {
          id: frameID,
          action: 'MICROCMS_POST_DATA',
          message: {
            id: 'multiTextarea',
            title: 'タブ切り替えフィールド',
            description: '複数のテキストエリアをタブで切り替えて入力するフィールドです。',
            data: tabInputValueObj
          }
        },
        microCMSOrigin
      );
    }

    /** フィールドのサイズを更新する */
    function updateEditorSize() {
      const fieldWrapElm = document.getElementById('multiInputFieldWrap')
      const fieldHeight = fieldWrapElm.clientHeight
      window.parent.postMessage(
        {
          id: frameID,
          action: 'MICROCMS_UPDATE_STYLE',
          message: {
            height: `${fieldHeight + 10}px`,
            width: '100%'
          }
        },
        microCMSOrigin
      );
    }
  </script>
</body>

</html>
```

</details>

## 利用サービス・ライブラリ
- microCMS 拡張フィールド
  https://document.microcms.io/manual/field-extension  
  ※React慣れてる方は[microCMSの拡張フィールド開発用SDK](https://blog.microcms.io/microcms-field-extension-sdk/)を使っても良いかも。
  <br>
- Material Web  
  Googleのデザインシステム「Material Design」をベースに作られたUIを、Webで手軽に利用できるフレームワーク。  
  https://material-web.dev/about/intro/  
  https://github.com/material-components/material-web  
  <br>
- Quill
  オープンソースのWYSIWYGエディタライブラリ。  
  今回はシンプルにテキスト入力のみ利用。  
  https://quilljs.com/