💧

TypeScript を使い Drupal 9.3 以降で CKEditor 5 向けのプラグインを登録してみる

2022/04/28に公開

Drupal 9.3 になり CKEditor 5 が同梱されるようになりました。CKEditor 5 のプラグインは CKEditor 4 から大きく変わり、JavaScript を書くだけでは足りなくなってしまいました。そこで、この記事では CKEditor 5 に TypeScript で書いたプラグインを登録するところまでを、デモリポジトリのソースコードを基に説明します。

説明用デモレポジトリ

https://gitlab.com/tom-konda/drupal-ckeditor-5-typescript-demo

下準備

CKEditor 5 のプラグイン開発のため、package.json に必要なライブラリや型定義を記述していき、yarn を使ってインストールします。
CKEditor 5 のプラグイン開発では、CKEditor 4 のようにブラウザ用の JavaScript を書くだけでは完結せず、Webpack を使って適切な形に変換する必要があるなど開発方法がかなり変更されています。

package.json
{
  "name": "cke5_ts_test",
  "engines": {
    "yarn": ">= 1.6",
    "node": ">= 16.0"
  },
  "scripts": {
    "build": "yarn build:transpile & yarn build:webpack",
    "build:transpile": "cross-env BABEL_ENV=modern node ./develop/transpile.mjs",
    "build:webpack": "webpack --config ./webpack.config.js"
  },
  "dependencies": {},
  "devDependencies": {
    "@babel/core": "^7.17.8",
    "@babel/preset-env": "^7.16.11",
    "@babel/preset-typescript": "^7.16.7",
    "@types/ckeditor__ckeditor5-core": "^33.0.2",
    "@types/node": "^17.0.21",
    "ckeditor5": "^33.0.0",
    "cross-env": "^7.0.3",
    "fast-glob": "^3.2.11",
    "terser-webpack-plugin": "^5.3.1",
    "typescript": "^4.6.3",
    "webpack": "^5.70.0",
    "webpack-cli": "^4.9.2"
  }
}

プラグイン開発

TypeScript から JavaScript への変換

CKEditor 5 にプラグインとして登録するためには、下記のようにプラグインのキーと実装クラスを紐付けてデフォルトエクスポートする必要があります。

export default {
  PluginKey: PluginClass,
}

デフォルトエクスポート部分は下記のように実装し、実装クラスはコードを整理しやすくするため別ファイルからインポートするようにします。

/ts/src/cke5-test-plugin.ts
import { TestPlugin } from "./plugin-body"

export default {
  TestPlugin,
}

実装クラスは CKEditor 5 のプラグインクラスを継承する必要がありますが、Drupal 側の実装を参考に ckeditor5/src/core からクラスをインポートしています。
実装クラス側は最低限のコードさえ有れば良いので、呼び出されたときに console.log() を実行するだけの init() メソッドを実装します。

/ts/src/plugin-body.ts
import { Plugin } from 'ckeditor5/src/core';

export class TestPlugin extends Plugin {
  init() {
    console.log('Test plugin is initialized.');
  }

  static get pluginName() {
    return 'TestPlugin';
  }
}

ここで、問題が生じます。ckeditor5/src/core は TypeScript の型定義ではないため、このままだとエラーが出てしまいます。エラー解決のため、tsconfig.json 側でパスの読み替えを行う設定をします。
tsconfig.jsonbaseUrl を設定した上で paths を設定すると、パスの読み替えが行われるようになるので、ckeditor5/src/core@types/ckeditor__ckeditor5-core を割り当てます。これでエラーは解消されます。

tsconfig.json
    "paths": {
      "ckeditor5/src/core": [
        "./node_modules/@types/ckeditor__ckeditor5-core"
      ],
    },

TypeScript から JavaScript への変換は以前投稿した Non-decoupled Drupal プロジェクトでの TypeScript によるフロントエンド開発環境の試作の際に作成した、デモリポジトリの実装をほぼそのまま使っています。

Webpack の設定とライブラリ読み込みの設定

Webpack の設定は Drupal に添付されている設定を流用して下記を満たすようにします。

  • 変換対象のファイルは各プラグインの起点となるファイルを設定
  • 出力は所定の形式にする
    • umd 形式
    • 出力先は libraries.yml で設定するファイル
    • library は、[CKEditor5, plugin_name]
    • 上記の plugin_name は ckeditor5.yml と同値
  • プラグインに DLL プラグインを指定する

今回は、一つしかプラグインがないため Drupal の設定を少し変更し、下記のようにします。

webpack.config.js
const path = require('path');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');

module.exports = [{
  mode: 'production',
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          format: {
            comments: false,
          },
        },
        test: /\.js(\?.*)?$/i,
        extractComments: false,
      }),
    ],
    moduleIds: 'named',
  },
  entry: {
    path: path.resolve(
      __dirname,
      'js/src/cke5-test-plugin.js',
    ),
  },
  output: {
    path: path.resolve(__dirname, 'js/dist'),
    filename: 'cke5-test-plugin.js',
    library: ['CKEditor5', 'testPlugin'],
    libraryTarget: 'umd',
    libraryExport: 'default',
  },
  plugins: [
    new webpack.BannerPlugin('cspell:disable'),
    new webpack.DllReferencePlugin({
      manifest: require(path.resolve(__dirname, './node_modules/ckeditor5/build/ckeditor5-dll.manifest.json')), // eslint-disable-line global-require, import/no-unresolved
      scope: 'ckeditor5/src',
      name: 'CKEditor5.dll',
    }),
  ],
  module: {
    rules: [{ test: /\.svg$/, use: 'raw-loader' }],
  },
}];

ライブラリの読み込み設定は通常のライブラリと変わりありませんが、下記の点は留意する必要があります。

  • CKEditor 5 のプラグインなので、依存関係に core/ckeditor5 を入れる
  • Drupal 標準の集約機能有効時に意図しない動作を避けるため、preprocess: true を設定する
  • ライブラリの JavaScript は TerserPlugin で minify されているので、minified: true を設定する
    • minimize: true が設定されているときのみ

プラグイン読み込みの設定

プラグインの実装である JavaScript 側の設定は終わったので、プラグインを読み込むための設定を追加します。Drupal 9.3 から CKEditor 5 のプラグイン設定用に module_name.ckeditor5.yml というファイルが追加されました。このファイルに所定の形式に従って設定を書いていきます。
所定の形式に関しては、Drupal.org の記事プラグイン定義のソースコードに記述されています。プラグイン定義のソースコードに書かれた例外をみれば、命名規則や必須項目などを確認することができるので設定の際はこのファイルを参照すれば最低限の設定を満たすことが出来ます。
下に module_name.ckeditor5.yml の例を挙げます。

cke5_ts_test.ckeditor5.yml
cke5_ts_test_test_plugin:
  ckeditor5:
    plugins:
    - testPlugin.TestPlugin
  drupal:
    label: "Test CKEditor 5 plugin"
    library: cke5_ts_test/ckeditor5_test_plugin
    elements: false

注意点として、ckeditor5.plugins の値は Webpack の設定で出てきた library の値に依存する点です。ckeditor5.plugins の値は plugin_name.ClassName の形で指定しますが、この plugin_namelibrary の配列の1番目の要素と等しくなります。よって、Webpack と module_name.ckeditor5.yml のどちらかの設定を変えた際は変え忘れがないかを確認した方が良いと思います。
module_name.ckeditor5.yml の他の項目とその詳細に関しては、上記の Drupal.org の記事を確認してください。

まとめ

この記事では、Drupal に同梱される CKEditor 5 のプラグイン開発を TypeScript で行い CKEditor 5 にプラグイン登録するまでの手順を解説しました。この記事で書かれている内容は登録までなので、CKEditor 5 のエディタ自体の拡張は公式の API ドキュメントを読み進めて行う必要があります。
プラグイン開発の方式が大幅に変わってしまったためか、2022/04 時点で CKEditor 5 対応のモジュールは CKEditor Tweetable Text のみで CKEditor 4 を拡張している貢献モジュールの CKEditor 5 対応状況はまだまだです。次期リリースの Drupal 10 に標準で含まれる WYSIWYG は CKEditor 5 のみになってしまうので、もし、Drupal 10 時点で CKEditor 5 に対応したプラグインが必要ならばソースコードに対する積極的な貢献をすべきでしょう。ただし、CKEditor 4 を拡張しているモジュールが CKEditor 5 で廃止されたような機能を基に拡張していた場合は厳しいかもしれません。

Discussion