WebTecNote

[WordPress] TinyMCE(v5) カスタムボタンの追加とショートコード/HTMLの相互変換

TinyMCEにショートコードを挿入するカスタムボタンを追加したはいいが、
見た目がイケてないのでビジュアルエディタではHTMLタグに変換してスタイリングしたいなあと思いましてね、その方法をぐぐってたら自分が昔書いた記事にたどり着くっていう…

[wp] 自作したショートコードをビジュアルエディタ内で置換する

そりゃ9年も経ってたら書いたことすら忘れますわ。

TinyMCE自体も記事を書いた当時多分バージョン3だったのが今はバージョン5になってて記事のソースじゃ動かないんで、こちらに現行のWordpress(5.x系)で動くものを載せておきます。

WordPressのTinyMCEカスタムボタンの追加

なんとWordpressの方は11年前に書いた記事の内容が今だに生きてます。

[WP]TinyMCEのビジュアルリッチエディタにカスタムボタンを追加する

PHPもバージョンが変わって、現在は匿名関数が使えますね。

// functions.php
add_action('init', function () {
  add_filter( "mce_external_plugins", function ( $plugin_array ) {
      // ①tinymce.PluginManager.addで設定した名前
      $plugin_array['sample'] = get_template_directory_uri() . '/js/tinymce-plugin.js';
      return $plugin_array;
  });
  add_filter( 'mce_buttons', function ( $buttons ) {
      // ② Editor.addButtonで設定した名前
      array_push( $buttons, 'sampleButton' );
      return $buttons;
  });
});

TinyMCEプラグイン作成

これを書いたWordpress5.2.2に入ってるTinyMCEはv4.9.4みたいです。
以下v5のAPIリファレンスにリンクしていますが、v4系独自のものについてだけv4のAPIドキュメントへ誘導します。

上記mce_external_pluginsで登録するTinyMCEプラグインは、テーマディレクトリ内の/js/tinymce-plugin.jsで、
そのプラグインファイルでsampleButtonという名前のボタンを追加するという内容です。

ボタンの挙動は次のようにします:

  1. ボタンを押す
  2. TinyMCEのポップアップを開く
    セレクトボックスで色、テキストボックスでテキストを入力する
  3. 2で入力した内容をショードコードで挿入
    [sample color="primary"]テキスト[/sample]
  4. ビジュアルエディタで3をHTMLタグに変換
    <span class="sample-button" data-color="primary">テキスト</sample>

プラグインの大枠はあまり変わってなくて、これです:

(function() {
    tinymce.create('tinymce.plugins.Sample', {
        /**
         * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
         * @param {string} url Absolute URL to where the plugin is located.
         */
        init : function(ed, url) {
          //ここで作る
     },
    });
    // ① mce_external_plugins で使う名前
    tinymce.PluginManager.add( 'sample', tinymce.plugins.Sample );
})();

tinymceはWordpressからグローバルできてるのでそのまま使える。

このinitのfunction中にボタンとかを追加していく。

カスタムボタンの追加

TinyMCEのカスタムボタンはEditor.addButtonで追加できる:

// ② mce_buttons で使う名前
ed.addButton('sampleButton', {
  title : 'サンプルのボタン',
  classes: 'btn widget',
  cmd: 'sample_popup',
  tooltip: 'サンプルのボタンです',
  image : url.replace('/js', '') + '/images/sample-button.png',
});

imageでアイコン画像を指定できるが、代わりにアイコンも使える。

icon: 'alignright'

ポップアップウィンドウの設定

TinyMCEv5のポップアップは、コンテンツの指定がiFrame、HTML、DialogComponentsの3種類あるが、
ここではDialogComponentsを利用するものを載せてます。

Editor.addCommandでポップアップを開くコマンドを追加する

// @params {Object} val コマンド実行時に渡される値
ed.addCommand('sample_popup', function (ui, val) {
  let text = '';
  let color = '';
  if (val && val.text) text = val.text;
  if (val && val.color) color = val.color;

  ed.windowManager.open({
    title: 'サンプルポップアップ',
    body: [
      {
        type: 'listbox',
        name: 'color',
        label: '色',
        value: color,
      'values': [
          {text: '青色', value: 'primary'},
          {text: '灰色', value: 'secondary'},
          {text: '黄色', value: 'warning'},
          {text: '赤色', value: 'danger'},
          {text: '黒色', value: 'dark'},
        ],
      },
      {
        type: 'textbox',
        name: 'text',
        label: 'リンクするテキスト',
        value: text
      }
    ]
  });
});

UIはv4とv5で利用できるものが違い、上記はv4のものになります。
何が使えるかはv4のAPIリファレンスを参照ください。

これでおなじみのポップアップが開くようになります

ポップアップにonSubmitイベント追加

ポップアップで入力後、OKボタンが押された時のイベントを追加する。

フォームに入力された値はcallback関数のeventにdataプロパティとして渡されているので、
Editor.insertContentでショートコードに書き換えた物を突っ込めばおk。

// body:[]の下
onsubmit: function( e ) {
  ed.insertContent(`[sample color="${e.data.color}"]${e.data.text}[/sample]`);
}

カーソルがあった場所にショートコードが表示されたら次へ。

ショートコードとHTMLの相互置換

TinyMCEのエディタはイベントハンドラが色々あって、その中にコンテンツに内容をセットする前に実行するBeforeSetContentがあります。

コールバック関数に渡されるイベントオブジェクトのcontentプロパティで入力されている内容が取れるので、ここでショートコードをHTMLにreplaceする。

ed.on('BeforeSetcontent', function(e){
  e.content = e.content.replace(/\[sample color="(\w+)"](.+)\[\/sample\]/g, function(all, color, text) {
    return `<span class="sample-button" data-color="${color}">${text}</span>`;
  });
});

HTMLからショートコードに戻すのはGetContentイベントでやる。

ed.on('GetContent', function(e){
  e.content = e.content.replace(/<span class="sample-button" data-color="(\w+)">(.+)<\/span>/g, function(all, color, text) {
    return `[sample color="${color}"]${text}[/sample]`;
  });
});

これが動作するとビジュアルとテキスト(ソースコード)を行き来した時に、HTMLタグとショートコードがそれぞれ表示されるようになります。

エディタースタイルの追加

ビジュアルエディタにスタイルシートを追加して、

add_editor_style( 'css/custom-editor-style.css' );

その中にカスタムボタンが生成するタグに対するスタイルを書けば、

.sample-button {
  display: inline-block;
  border: solid 2px;
  border-radius: .5rem;
  padding: 0 .5rem;
}
.sample-button[data-color="primary"] {
  color: blue;
}

反映されます。

一応これで完成。

ダブルクリックで編集する

せや!ビジュアルエディタで置換後のNodeをダブルクリックしたら編集できるようにしよ!

エディタにDblClickイベントを追加。

ed.on('DblClick',function(e) {
  if ( e.target.className.indexOf('sample-button') > -1 ) {
    ed.execCommand('sample_popup','', {
      color : e.target.attributes['data-color'].value,
      text: e.target.innerHTML
    });
  }
});

execCommandでsample_popupを起動して、タグに設定されている値を渡す。

ポップアップopenイベント追加

Editor.selection.getNode()でポップアップが開いた時に選択されているNodeが取れる。
この要素をチェックしてクラスにsample-buttonがあったらselectNodeとして保存しておきます。

onopen: function() {
  const selectNode = ed.selection.getNode();
  if (selectNode.className.indexOf('sample-button') > -1) {
    this.selectNode = selectNode;
  }
},

ポップアップsubmitイベント変更

単にショートコードをinsertしていただけのポップアップsubmitイベントに、編集用のロジックを追加する。

onsubmit: function( e ) {
  // 編集
  if (this.selectNode) {
    let newNode = ed.dom.create(
      'span',
      {
        class: 'sample-button',
        'data-color': e.data.color,
      },
      e.data.text
    );
    // DOM置換
    ed.dom.replace(newNode, this.selectNode, false);
    // カーソル位置セット
    ed.selection.setCursorLocation(newNode, 1);
    this.selectNode = null;
  } else {
    ed.insertContent(`[sample dcolor="${e.data.color}"]${e.data.text}[/sample]`);
  }
}

openイベントで選択されたNodeを保存するので、それの有無で編集かどうかを判断。
Editor.dom.createで編集後のNodeを生成して、
Editor.dom.replaceで入れ替える。
Editor.selection.setCursorLocationでカーソル位置を戻せば
ポップアップから編集できるようになります。

その他のカスタマイズ

その他のカスタマイズについての補足説明。

履歴に残す

undoManagerのメソッドを利用します。
新しく履歴を追加する場合はadd()で、何かしら編集を加えた後に実行する。

ed.undoManager.add();

ダブルクリックで削除する

dom.remove()使います。

ed.on('DblClick',function(e) {
  if ( e.target.classList.contains('sample-button')) {
    ed.dom.remove(e.target);
  }
});

ポップアップウィンドウのボタンを変更する

bodyと同じ要領でbuttonsを設定すればおk。
以下は編集中の要素を削除するボタンを追加する例。

buttons: [
  {
    text: 'OK',
    subtype: 'primary',
    onclick: 'submit'
  },
  {
    text: 'キャンセル',
    onclick: 'close'
  },
  {
    text: '削除',
    icon: 'remove',
    subtype: 'delete',
    onclick: () => {
      if (ed.selection.getNode()) {
        ed.dom.remove(ed.selection.getNode());
        ed.undoManager.add();
      }
      ed.windowManager.close();
    }
  }
],

ショートコードの追加

ショートコードAPI仕様メモ をみてください。

おことわり

Gutenbergは使ってないので確認すらしてません。

参考リンク

モバイルバージョンを終了