【第6回】Tiptap 拡張とカスタムボタンで独自機能を追加 - MTAppjQuery で Movable Type 9 のリッチテキストエディタをカスタマイズ
目次
この記事は「MTAppjQuery で Movable Type 9 のリッチテキストエディタをカスタマイズ」の第6回です。今回は、エディタに独自の機能を追加する2つの方法をご紹介します。Tiptap 拡張の自作と、ツールバーへのカスタムボタンの追加です。
コードを書き込む場所(自由テキストエリア)とベースコードについては第1回をご覧ください。
Tiptap の API を使えるようにする
第5回でお伝えしたとおり、MTRichTextEditor の土台は Tiptap というエディタフレームワークです。そして MTRichTextEditor は、Tiptap のコア API を取り出すための入口を公式に用意してくれています。
MTRichTextEditor.import('@tiptap/core').then(({ Extension, InputRule }) => {
// ここで Tiptap の API が使える
});
MTRichTextEditor.import('@tiptap/core') は Promise を返し、Tiptap の Extension や InputRule といったクラスにアクセスできるようになります。npm でパッケージを入れたりビルド環境を用意したりする必要はありません。自由テキストエリアに書くコードだけで完結します。
なお、import に指定できるモジュール名は @tiptap/core です。それ以外の名前を渡すとエラーになります。
入力パターンで自動変換する拡張を作る
公式の開発者向けガイドに載っている、分かりやすい例から始めましょう。:+1: と入力すると絵文字の 👍 に自動変換される拡張です。
<script type="module">
if (window.MTRichTextEditor) {
MTRichTextEditor.import('@tiptap/core').then(({ Extension, InputRule }) => {
const thumbsUpExtension = Extension.create({
name: 'thumbsUp',
addInputRules() {
return [
new InputRule({
find: /:\+1:/,
handler: ({ state, range }) => {
state.tr.insertText('👍', range.from, range.to);
},
}),
];
},
});
MTRichTextEditor.on('create', (options) => {
options.extensions.push(thumbsUpExtension);
});
});
}
</script>
流れを整理すると次のとおりです。
MTRichTextEditor.import('@tiptap/core')でExtensionとInputRuleを取り出すExtension.create()で拡張を定義する。addInputRules()は「この正規表現にマッチしたらこう処理する」という入力ルールを返すcreateイベントでoptions.extensionsに追加する
InputRule は Markdown 記法の自動変換と同じ仕組みなので、応用範囲は広いです。社内でよく使う記号の変換、定型文の展開など、入力の手数を減らすカスタマイズに向いています。
ツールバーにカスタムボタンを追加する
もうひとつの方法が、ツールバーへの独自ボタンの追加です。MTRichTextEditor のツールバーボタンは Web Components(カスタム要素)として実装されていて、自分でカスタム要素を定義して登録できます。
要素名は mt-rich-text-editor-toolbar-item- に続けてボタン名を付けるという命名規則です。ベースクラスの ToolbarItemElement は MTRichTextEditor.Component から取り出します。
定型の挨拶文を挿入するボタンの例です。
<script type="module">
if (window.MTRichTextEditor) {
const { ToolbarItemElement } = MTRichTextEditor.Component;
// ボタン名 greeting のカスタム要素を定義
customElements.define(
'mt-rich-text-editor-toolbar-item-greeting',
class extends ToolbarItemElement {
constructor() {
super();
const button = document.createElement('button');
button.title = '定型の挨拶文を挿入';
button.innerHTML =
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">' +
'<path fill="currentColor" d="M3 4h18v13H8l-5 4V4zm3 4v2h12V8H6zm0 4v2h8v-2H6z"></path>' +
'</svg>';
button.addEventListener('click', () => {
this.tiptap?.commands.insertContent(
'<p>いつもお読みいただきありがとうございます。</p>'
);
});
// ボタンは shadowRoot に追加する
this.shadowRoot.appendChild(button);
}
connectedCallback() {
super.connectedCallback();
}
}
);
MTRichTextEditor.on('create', (options) => {
// 2行目 > 左寄せエリア の末尾にボタンを追加
const toolbar = structuredClone(options.toolbar);
toolbar[1][0].push(['greeting']);
options.toolbar = toolbar;
});
}
</script>
ToolbarItemElement が用意してくれるのはボタン用のスタイルだけなので、constructor の中で <button> を作って this.shadowRoot に追加します。ここを忘れると中身のない幅 0 のボタンになり、ツールバーには何も表示されません。公式のサンプルプラグインも同じ書き方です。
クリック時の処理では this.tiptap から Tiptap のコマンド API を呼び出しています。insertContent はカーソル位置に HTML を挿入するコマンドです。あとはボタン名(ここでは greeting)を第2回で学んだ要領でツールバー配列に加えれば、ツールバーに表示されます。
ツールバーを structuredClone で複製してから書き換えているのは、第2回で触れたとおり options.toolbar が create の呼び出しごとに同じ配列で渡ってくるためです。直接 push するとリッチテキスト欄の数だけボタンが増えてしまいます。一方で先ほどの options.extensions は呼び出しごとに別の配列が渡ってくるので、そのまま push して構いません。
ツールバーの構成は設定画面の内容によって変わるので、options.toolbar[1][0] の添字はお使いの環境に合わせて調整してください。
公式のサンプルプラグインも参考に
シックス・アパートからは、エディタ拡張の公式サンプルプラグイン集が公開されています。絵文字パレットやフォントサイズ変更など、実践的な実装例が揃っているので、ここまでの内容を理解した上で読むととても勉強になります。
サンプルは Movable Type のプラグイン形式ですが、JavaScript 部分の書き方は今回ご紹介した方法と同じです。気に入った機能があれば、JavaScript 部分を MTAppjQuery の自由テキストエリアに移植して使うこともできます。
まとめ
今回のポイントを整理しておきます。
MTRichTextEditor.import('@tiptap/core')で Tiptap のコア API をビルド環境なしで使えるExtension.create()とInputRuleで入力の自動変換が作れる。作った拡張はoptions.extensions.push()で追加する- カスタムボタンは
MTRichTextEditor.Component.ToolbarItemElementを継承したカスタム要素として定義する。要素名はmt-rich-text-editor-toolbar-item-ボタン名で、ボタン本体はconstructorでshadowRootに追加する - ツールバーに加えるときは
options.toolbarを複製してから書き換える(options.extensionsはpushで構わない) - 公式サンプルプラグイン集が実践例の宝庫
次回は連載の最終回として、これまでのカスタマイズをサイトごと・画面ごとに出し分ける方法を取り上げます。
今回は以上となります。