MTAppjQuery+PHPできめ細かな定型文管理機能を実装する

これは[Movable Type Advent Calendar 2020](https://adventar.org/calendars/4971)の10日目の記事です。

# Movable Typeの定型文機能について

Movable Typeには、標準で[定型文の管理機能](https://www.movabletype.jp/documentation/mt7/users-guide/manage-content/manage-entries/manage-boilerplate/)が搭載されています。
よく使う文章やパーツをあらかじめ定型文として登録しておき、
リッチエディタの「定型文の挿入」ボタンから読み込む事ができる便利な機能です。
（内部的にはTinyMCEのTemplateプラグインが利用されています。）

最近では、ブロックエディタやMTAppjQuery2のマルチフィールド機能の登場によって、
よりシンプルで直感的な編集ができるようになりましたが、
要件等によっては、依然としてリッチエディタが必要とされるケースが出てきますし、
定型文機能についてもまだまだ重宝されると思っています。

しかし、MT標準の定型文機能にはいくつかの制限があり、時おり不便に感じる事があります。

## MT標準の定型文機能の制限

### 記事の編集画面でしか使用できない

MTでは「記事」「ウェブページ」「コンテンツデータ（MT7から）」の3種類のコンテンツ管理方法がありますが、
このうち、定型文機能を使用できるのは記事の編集画面のみとなります。
ウェブページやコンテンツデータの編集画面では、リッチエディタに定型文を読み込む事ができません。

### サイト内の定型文しか使用できない

定型文は、サイト/子サイト（MT6で言うところのウェブサイト/ブログ）ごとに管理が分かれており、
別のサイトや子サイトに登録した定型文を読み込む事ができません。
そのため、サイトを跨いで共通利用したい定型文がある場合は、すべてのサイトに対して同じ定型文を登録する必要があります。
定型文を修正する際も、すべてのサイトの定型文に対して修正を行う必要があるため、非常に手間がかかってしまいます。

### 並べ替えができない

リッチエディタに定型文を挿入する際のプルダウンの表示順は、
上から「定型文の登録が新しい順」で固定となっています。
定型文を種別ごとにまとめたり、使用頻度が高いものを上に表示させたりする事はできません。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529502/7c8f04fe-2e33-4349-2455-4b07203326b7.png" width="70%">

--------------------
今回は、MT標準の定型文機能の代わりに独自の定型文管理機能を実装し、
上記のような制限を受けず、きめ細やかに定型文を管理する方法についてご紹介します。

# 実装の概要

リッチエディタ（TinyMCE）が取得する定型文のリストは、
`MT.Editor.TinyMCE.config.templates`に定義されています。
通常ここには、MTの標準機能で登録された定型文が配列形式で入る形となっておりますが、
これを上書きする事で、任意の定型文を取得させる事が可能となります。

また、ここには配列ではなくURLを指定する事が可能となっており、
そのURLが返すJSONを、定型文データとして取得させる事ができます。

そこで今回は、「定型文のリストをJSONで返すAPI」を作成し、
定型文を動的に取得させる仕組みを作ります。

また、定型文はサイトごとの管理ではなく一括で管理できるようにし、
かつ定型文ごとに使用可能な編集画面やサイトをまとめて設定できるようにします。

# 制限事項

* 対応するMTのバージョンは7系または6.7.1以降となります。
  ※MT6系の場合は、旧バージョンのTinyMCEを使用する設定が【無効】となっている必要があります。

* 本記事のカスタマイズを行うと、MTの標準機能で登録した定型文は使用できなくなります。
既に登録済みの定型文がある場合は、改めて本記事の定型文管理機能を用いて登録する必要があります。

# 必要なもの

* [MTAppjQuery](https://bit-part.net/products/mtappjquery/)プラグイン

MT6の場合はv1系、MT7の場合はv2系でないと動作しないためご注意ください。

また、今回はPHPを使用するため、MTが稼働しているサーバにPHPがインストールされている必要があります。

# 実装方法

以下のディレクトリ構成を前提として説明します。

* ドキュメントルートディレクトリ
/var/sites/test/htdocs/

* MTのインストールディレクトリ ※ApacheでAliasを設定
/var/sites/test/mt/

## 定型文管理用のサイトを作成する

まず、定型文を一括管理するためのサイト（MT6の場合はウェブサイト）を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529502/7058c211-990a-dab1-d5f4-405aab693bf6.png)

| 項目 | 値 |
|:-:|:-:|
|サイトテーマ|Mont-Blanc
|サイト名|定型文管理
|サイトURL|サイトトップのURL
|サイトパス|ドキュメントルート<br>（今回は/var/sites/test/htdocs）|

サイトテーマは、プレーンテーマ「Mont-Blanc」を推奨します。
他のテーマでも問題ありませんが、テーマに同梱されているアーカイブテンプレートやインデックステンプレートは使用しないため、サイトの作成後に削除してください。

**このサイトで作成した「記事」を、定型文のデータとして扱います。**
（少々ややこしいですが、標準の定型文機能は一切使用しません。）

### カスタムフィールドを作成する

定型文ごとに、利用可能な編集画面やサイトの情報を保持するため、
定型文管理サイトに下記2つのカスタムフィールドを作成します。

* 「次の編集画面で有効」フィールド

| 項目 | 値 |
|:-:|:-:|
| システムオブジェクト| 記事 |
| 名前  | 次の編集画面で有効  |
| 種類  | テキスト |
| 必須  | オフ |
| ベースネーム  | tt_screen  |
| テンプレートタグ  | tt_screen  |


* 「次のサイトで有効」フィールド

| 項目 | 値 |
|:-:|:-:|
| システムオブジェクト| 記事 |
| 名前  | 次のサイトで有効  |
| 種類  | テキスト  |
| 必須  | オフ |
| ベースネーム  | tt_site  |
| テンプレートタグ  | tt_site  |

## 編集画面をカスタマイズする

続いて、定型文管理サイトの記事の編集画面をMTAppjQueryでカスタマイズします。

### user.js 

user.jsに下記を追加します。

```javascript:user.js

// 定型文管理サイトのID（適宜変更してください）
const tinyMCETemplateSiteID = 1;

if(mtappVars.blog_id == tinyMCETemplateSiteID  && mtappVars.screen_id == 'edit-entry'){

  // フィールドの並べ替え
  mtapp.fieldSort({
    sort: 'title,text,excerpt,customfield_tt_screen,customfield_tt_site',
    otherFieldHide: true
  });

  /*
   * 「次の編集画面で有効」フィールドのカスタマイズ
   */
   
  // フィールドを複数選択チェックボックスに変更
  $('#customfield_tt_screen').mtapp('multiCheckbox', {
    label: {
      'edit-entry' : '記事',
      'edit-page' : 'ウェブページ',
      'edit-content-type-data' : 'コンテンツデータ' // MT6の場合は不要
    }
  });

  /*
   * 「次のサイトで有効」フィールドのカスタマイズ
   */

  // ローディングメッセージの表示
  const loading_span = $('<span><img src="/mt/mt-static/images/indicator.white.gif" />サイト情報を取得中です...</span>').insertBefore($('#customfield_tt_site'));

  // Data APIで全サイトの情報を取得
  $.ajax({
    url: '/mt/mt-data-api.cgi/v3/sites',
    data: { limit: 999999, fields: 'id,name' }
  }).done(function(res){

    // 取得したサイト情報をもとに、multiCheckboxのラベル用データを作成
    const mc_label = {};
    $.each(res.items, function(key,val){
      mc_label[val.id] = val.name;
    });

    // フィールドを複数選択チェックボックスに変更
    $('#customfield_tt_site').mtapp('multiCheckbox', {
      label: mc_label
    });

    // ローディングメッセージを削除
    loading_span.remove();

  }).fail(function(){
    loading_span.text('エラー：サイト情報の取得に失敗しました。');
  });

}
```

#### 解説

`mtapp.fieldSort`でフィールドの並べ替えを行い、
`.mtapp('multiCheckbox')`で、先程作成したカスタムフィールドを複数選択チェックボックス形式に変更します。
※MT6の場合は`$.MTAppFieldSort()`、`$.MTAppMultiCheckbox()`を使用してください。

「次の編集画面で有効」フィールドは、記事/ウェブページ/コンテンツデータから選択できるようにします。

「次のサイトで有効」フィールドは、CMSで管理しているサイトが選択肢となるように、Data APIで全サイトのIDと名前を取得します。
この時、Data APIのアクセス許可が無効となっているサイトは取得対象外となり、選択肢にも表示されないためご注意ください。
（サイトの「設定⇒Webサービス」にて、アクセスを許可する必要があります。）
ちなみにMT7 r.4605（MT6.5.3）より、サイト作成時のData APIのアクセス許可はデフォルトで無効となっています。

Data APIへのアクセスを許可したくない場合は、
インデックステンプレートでサイトの情報をJSON出力し、それを読み込ませる方法でもOKです。

### user.css

user.cssに下記を追加します。

```user.css

/*
 複数選択チェックボックスの選択肢同士の間隔を広げる
*/
#customfield_tt_screen-field .form-check-inline,
#customfield_tt_site-field .form-check-inline {
  margin-right: 24px;
}
/* MT6の場合 */
#customfield_tt_screen-field .mcb-label,
#customfield_tt_site-field .mcb-label {
  margin-right: 24px;
}

/*
 「次のサイトで有効」フィールドについて、元のテキストフィールドを非表示にする
*/
#customfield_tt_site {
  display: none;
}

/*
 プレビューボタンを非表示にする
*/
/* blog-id-1の部分は、定型文管理サイトのIDに適宜変更してください */
body.blog-id-1 button[name="preview_entry"] {
  display: none;
}

/*
 MT標準の定型文機能のメニュー項目を非表示にする
*/
#menu-entry_manage_formatted_text {
  display: none;
}
/* MT6の場合 */
#menu-entry li.item:nth-child(3) {
  display: none;
}
```

#### 解説

* 標準では複数選択チェックボックスの選択肢同士の間隔が若干狭いため、調整します。

* 「次のサイトで有効」フィールドについて、
Data APIによるデータ取得が完了するまでの間は元のテキストフィールドが見えてしまうため、始めから非表示としておきます。

* 記事のプレビュー機能は使用しないため、プレビューボタンを非表示にします。

* MT標準の定型文管理機能のメニュー項目を非表示にします。

-----

これらのカスタマイズによって、記事の編集画面は以下のようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529502/02d3a764-6fc7-2ffa-40f9-1ce67bec9721.png)
タイトルは定型文の名前、本文は定型文の内容、概要は定型文の説明を入力する欄となります。

上の画像の場合、
作成した定型文は「イベント」サイトの「記事またはウェブページ」の編集画面でのみ使用可能となります。

## 定型文データ取得用のAPIを作成する

続いて実装の概要で説明した、定型文のリストをJSONで返すAPIを作成します。

定型文管理サイトで、下記のインデックステンプレートを作成します。

```php:定型文データ取得用API
<?php
// 全定型文のリスト
const tinymceTemplates = [
<mt:Entries lastn="0" compress="1">
  [
    <mt:Ignore>有効な編集画面</mt:Ignore>
    <mt:tt_screen split="," setvar="tt_screen">
    "screen" => [<mt:If tag="tt_screen">"<mt:tt_screen replace=',','","'>"</mt:If>],
    <mt:Ignore>有効なサイトID</mt:Ignore>
    "site" => [<mt:If tag="tt_site">"<mt:tt_site replace=',','","'>"</mt:If>],
    <mt:Ignore>tinyMCEに渡す定型文のデータ</mt:Ignore>
    "template" => [
      "title" => "<mt:EntryTitle encode_php="qq">",
      "content" => "<mt:EntryBody encode_php="qq">",
      "description" => "<mt:EntryExcerpt no_generate="1" encode_php="qq">"
    ]
  ]<mt:Unless name="__last__">,</mt:Unless>
</mt:Entries>
];

// GETパラメータより編集画面ID、サイトIDを取得する
$screen_id = (isset($_GET['screen_id']) && is_string($_GET['screen_id'])) ? $_GET['screen_id'] : '';
$blog_id = (isset($_GET['blog_id']) && is_string($_GET['blog_id']) && preg_match("/^[0-9]+$/", $_GET['blog_id']) ) ? $_GET['blog_id'] : '';

// 編集画面ID、サイトIDの条件に合致する定型文を取得する
$filtered_templates = array_filter(tinymceTemplates, function($data) use($screen_id, $blog_id){
  return (in_array($screen_id, $data['screen'], true) === true && in_array($blog_id, $data['site'], true) === true);
});
$filtered_templates = array_column($filtered_templates, 'template');

// 取得した定型文をJSON形式で返す
header('Content-Type: application/json');
header('X-Content-Type-Options: nosniff');
echo json_encode($filtered_templates);
?>
```

テンプレートの公開プロファイルは「スタティック(既定)」とし、
記事（定型文）が作成/更新される度に再構築されるようにします。

### 解説

GETパラメータで編集画面IDとサイトIDを受け取り、
その条件に合致する定型文のデータを、JSON形式で返します。

### テンプレートの出力先について

このAPIはMTの管理画面でのみ使用するものなので、
APIの出力先をドキュメントルート配下ではなく、MTのインストールディレクトリ配下にします。

今回はインストールディレクトリ直下に「tinymce_template」ディレクトリを作成し、その中にAPIを出力します。
※出力ファイル名の先頭に「../」を付けると、基点となるサイトパスから遡ってパスを指定する事が可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/529502/6626c269-14c7-a0dd-17e5-9bdaf824a3cf.png)

## 定型文データの取得先をAPIに変更する

最後に、`MT.Editor.TinyMCE.config.templates`の値をAPIのURLに変更します。

システムダッシュボードより、プラグインの設定画面を開きます。
MTAppjQueryの設定タブを開き、**「システム全体に適用する設定」**内の「user.cssの読み込み直前」欄に下記を追加します。
※「システムの管理画面に適用する設定」と間違えないようご注意ください。
※MT6の場合は「変数 js_include に追加（\</head\>の直前）」欄に追加してください。

```javascript:MT.Editor.TinyMCE.configの上書き処理
<script>
// MT.Editorが定義されているかチェック
if(typeof MT.Editor !== 'undefined'){

  // tinyMCE設定オブジェクト
  const config = MT.Editor.TinyMCE.config;

  // templateプラグインを追加する
  const extend_plugins = config.plugins + ',template';

  // 定型文の挿入ボタンを追加する
  let extend_buttons1 = config.plugin_mt_wysiwyg_buttons1;
  if(extend_buttons1.indexOf('template') == -1){
    extend_buttons1 = extend_buttons1 + ' | template';
  }

  // 定型文データの取得先をAPIのURLに変更する（画面IDとサイトIDをGETパラメータで渡す）
  const extend_templates = '/mt/tinymce_template/?screen_id='+mtappVars.screen_id+'&blog_id='+mtappVars.blog_id;

  // tinyMCEの設定を上書きする
  jQuery.extend(config, {
    plugins: extend_plugins,
    plugin_mt_wysiwyg_buttons1: extend_buttons1,
    templates : extend_templates
  });
}
</script>
```

以上で実装は完了となります。

定型文管理サイトで作成した記事（定型文）が、
指定した編集画面,サイトのリッチエディタから読み込めるようになっている事を確認してください。

# 定型文の並べ替えについて

定型文データ取得用APIにて、配列`tinymceTemplates`に全定型文のデータを格納しておりました。
この配列への格納順がそのまま、定型文挿入時のプルダウンの表示順となります。

```php:定型文データ取得用API（抜粋）
// 全定型文のリスト
const tinymceTemplates = [
<mt:Entries lastn="0" compress="1">
  [
  ...(省略)...
  ]<mt:Unless name="__last__">,</mt:Unless>
</mt:Entries>
];
```

\<mt:Entries\>ブロックタグのデフォルトの出力順は「公開日の新しい順」なので、
プルダウンの表示順も「公開日の新しい順」となります。
つまり、記事（定型文）の公開日を変更する事で、プルダウンの表示順を並べ替える事が可能となります。
`$.MTAppSortableBatchEdit()`を使うと、一括編集画面でドラッグアンドドロップで並べ替えができて便利です。

また、上記の出力ロジックを変更すれば
公開日以外の特定のカスタムフィールドの値の順に表示させたり、カテゴリごとに定型文をまとめるなどの応用も可能です。

# その他

定型文を「記事」として管理する事で、
標準の定型文管理機能には無い、次のようなメリットが生まれます。

* 不要となった定型文はステータスを「未公開」にする事で、データを保持しつつプルダウンから取り下げる事ができます。

* 定型文のエクスポート/インポートが可能となります。

* 記事管理に関するプラグインの恩恵を、定型文管理機能でも受ける事ができます。例えば[Copy This Entry](https://plugins.movabletype.jp/yuji_takayama/copy-this-entry.html)プラグインを導入すれば、定型文の複製ができるようになります。

-----

以上となります。
定型文の管理に苦労されている方は、是非お試しください。