RailsアプリケーションにWYSIWYGエディタ（CKEditor）を導入する

## はじめに
こんにちは。[kosukein38](https://twitter.com/kosukein38)です。
業務でRails4系のアプリケーションにWYSIEIGエディタを導入する必要があったため、そのライブラリの導入手順やできることについて、まとめておきます。
※知識の整理目的も含まれるため、少々冗長になる可能性があります。適宜読み飛ばしていただければと思います。
こんなのをメールの本文入力フォームに導入します。
<img width="400" alt="editor.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1697316/6cfa5bb5-b399-af21-3a5d-949bbd60af12.jpeg">

## 対象者
- WYSIEIGエディタが何かということを知りたい方
- Rails4系のアプリケーションにWYSIEIGエディタを導入する方法が知りたい方

Rails6系以降はTrixというWYSIEIGエディタをベースにした、Action Textというリッチテキスト機能が導入されているため、Rails6系以降のアプリケーションに対してはこちらを利用するのが良いかと思います。

https://railsguides.jp/action_text_overview.html

## 結論
gem `ckeditor`とその拡張機能を使って、Rails4系アプリケーションにWYSIEIGエディタを導入します

https://github.com/galetahub/ckeditor

## 選定まで

### そもそもWYSIEIGエディタって？
- WYSIWYG（ウィジウィグ）は、「What You See Is What You Get」の略で、日本語では「見たまま得るもの」と訳されます。これは、コンピュータプログラムやエディタなどのユーザーインターフェースの一形式を指す用語です。
- ユーザーがコンテンツを編集する際に、実際の表示と非常に近い形で編集内容を確認することができるエディタであるため、特別な知識がなくても簡単にHTMLでメール文作成などが行えます。
- メールマガジンの配信機能を実装するにあたり、非エンジニアでも簡単にHTMLを書けるようにするため導入しました。


### どんな選択肢がありそう？

まず、工数をかけられないため、gemでRailsアプリケーションに入れられるもWYSIWYGエディタを探しました。調べた限り、以下のような選択肢がありそうです。

- Froala

https://github.com/froala/wysiwyg-rails

- Trix

https://github.com/basecamp/trix

- Summernote

https://github.com/summernote/summernote-rails

- CKEditor

https://github.com/galetahub/ckeditor

スター数でみるとTrixがもっとも多く`17.8k`ですが、[Trixは見出し追加などに改修が必要という情報](https://tanarizm.com/rails-wysiwyg-3)があったため、次点のCKEditor（スター数`2.2k`）を選びました。
リポジトリの改修が近年行われていないのは気になりますが、Rails4系への導入なので大きな問題はないだろうと予想し、こちらに決定しました。

## CKeditorの導入方法

### インストール
ますは公式の[README](https://github.com/galetahub/ckeditor#readme)を読んでみてください。

>- CKEditor version 4.x (https://ckeditor.com/ckeditor-4/)
>- Rails 5.x, 4.2.x integration
>- Files browser
>- HTML5 file uploader
>- Hooks for formtastic and simple_form forms generators
>- Integrated with authorization frameworks CanCanCan and Pundit

CKeditorは2023年8月現在、ver.5系が最新のようですが、このgemで入れられるのは4系になります。また、Railsのバージョンも5系or４系です。

今回導入するRailsアプリには元々画像アップロード機能で[CarrierWave](https://github.com/carrierwaveuploader/carrierwave)を導入していました。CarrierWave + ActiveRecordの構成でアップローダーモデルの管理を行いたかったので、READMEどおり、Gemfileに以下の記述を追加し、`$ bundle install`しました。

```Gemfile
gem 'carrierwave'
gem 'mini_magick'
```
その後、READMEにならい以下のコマンドを実行

```
$ rails generate ckeditor:install --orm=active_record --backend=carrierwave
```
そうすると、必要なモデル群やマイグレーションファイルが自動で作られます。
マイグレーションファイルが作られたので、`$ rails db:migrate`を実行します。
生成される`ckeditor_assetsテーブル`でアップロードされたファイルの情報を管理します。
モデルをautoloadするために、`application.rb　`に以下を追加します。

※READMEにある記述に加えて、以下のissueにあるような記述を加えないと、本番環境でアセット群を読み込んでくれませんでした。注意してください。

https://github.com/galetahub/ckeditor/issues/307

```ruby:config/application.rb
#・・・
    config.autoload_paths += %W(#{config.root}/app/models/ckeditor)
    config.assets.precompile += Ckeditor.assets
    config.assets.precompile += %w(ckeditor/*)
#・・・
```
Ckeditor::Engineをマウントするために、routes.rbに以下を追加します。

```ruby:config/routes.rb
mount Ckeditor::Engine => '/ckeditor'
```


### CDN経由でRailsアプリケーションにエディタを読み込む

以下をまず参照して、CDNで導入できるバージョン（4系）を確認
http://cdn.ckeditor.com/

```ruby:config/initializers/ckeditor.rb
Ckeditor.setup do |config|
  config.cdn_url = '//cdn.ckeditor.com/4.22.1/standard/ckeditor.js'
end
```

ビューに読み込ませるために`application.html.erb`に`javascript_include_tag`を追加

```ruby:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
# ・・・
    <%= javascript_include_tag Ckeditor.cdn_url %>
# ・・・
  </head>
# ・・・

```
ここまでで、基本設定はできていると思います。
CKEditorを導入したいビューファイル中のフォーム内で以下のように記述します。

```ruby
= form_for @page do |form|
  = form.cktext_area :notes, class: 'someclass', ckeditor: { language: 'uk'}
  = form.cktext_area :content, value: 'Default value', id: 'sometext'
  = cktext_area :page, :info, cols: 40, ckeditor: { uiColor: '#AADC6E', toolbar: 'mini' }
```

bootstrap-formを使っている場合は以下のように書けるようです。
```ruby
= bootstrap_form_for resource do |form|
  = form.cktext_area :text, ckeditor: { language: 'uk'}
```

### ファイルのアップロード先の修正
CarrierWaveを使っていて、画像ファイルのアップロード先をAmazon S3に指定したい場合は
`app/uploaders/ckeditor_attachment_file_uploader.rb`
`app/uploaders/ckeditor_picture_uploader.rb`

今回のRailsアプリにはgem `carrierwave-aws`が導入されおり、例えば以下のように追加しています。

```ruby:app/uploaders/ckeditor_picture_uploader.rb
class CkeditorPictureUploader < CarrierWave::Uploader::Base
  include Ckeditor::Backend::CarrierWave
  include CarrierWave::MiniMagick

  # Choose what kind of storage to use for this uploader:
  storage :aws

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/ckeditor/pictures/#{model.id}"
  end

  # Provide a default URL as a default if there hasn't been a file uploaded:
  # def default_url
  #   "/images/fallback/" + [version_name, "default.png"].compact.join('_')
  # end

  # Process files as they are uploaded:
  # process scale: [200, 300]
  #
  # def scale(width, height)
  #   # do something
  # end

  # Create different versions of your uploaded files:
  version :thumb do
    process resize_to_fill: [118, 100]
  end

  version :content do
    process resize_to_limit: [800, 800]
  end

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_white_list
    Ckeditor.image_file_types
  end
end

```


これらのファイル内でパスの設定をしてください。


## エディタのカスタマイズ、拡張機能の導入
以下は、エディタ表示を変えたいなどエディタをカスタマイズしたい方、拡張機能を入れたい方はご覧ください。

### アセットパスの設定と`config.js`の作成

CKEditorは`ckeditor/config.js`ファイルを起点にエディタの設定を行います。
Railsアプリのプリコンパイル対象にするため、`config/initializers/assets.rb`に以下を追加します。


```ruby:config/initializers/assets.rb
Rails.application.config.assets.precompile += %w[ckeditor/config.js]
```

ここでJavaScriotファイル群の置き場について補足ですが、外部のJavaScriptライブラリを導入しているため、`vendor/assets/javascripts`のディレクトリを作成し、その中にファイルを配置します。`config.js`はtouchコマンドを使ってこのディレクトリ配下に配置します。
```
$ touch vendor/assets/javascripts/ckeditor/config.js
```

このあと、拡張機能のインストールを行いますが、先にconfig.jsの設定ファイルを示します。[デフォルトのconfig.js設定](https://github.com/galetahub/ckeditor/blob/master/app/assets/javascripts/ckeditor/config.js)を上書きすることで、Editorへの拡張機能や、表示するボタンなど設定することができます。設定の詳細は以下をご覧ください。
https://ckeditor.com/docs/ckeditor4/latest/guide/dev_configuration.html

```JavaScript:vendor/assets/javascripts/ckeditor/config.js

CKEDITOR.plugins.addExternal( 'fakeobjects', '/assets/ckeditor/plugins/fakeobjects/', 'plugin.js' );
CKEDITOR.plugins.addExternal( 'iframe', '/assets/ckeditor/plugins/iframe/', 'plugin.js' );
CKEDITOR.plugins.addExternal( 'autogrow', '/assets/ckeditor/plugins/autogrow/', 'plugin.js' );

CKEDITOR.editorConfig = function (config) {
  config.filebrowserImageBrowseLinkUrl = "/ckeditor/pictures";
  config.filebrowserImageBrowseUrl = "/ckeditor/pictures";
  config.filebrowserImageUploadUrl = "/ckeditor/pictures?";
  config.filebrowserUploadUrl = "/ckeditor/attachment_files";
  config.allowedContent = true;
  config.disallowedContent = 'img{width,height}';
  config.extraAllowedContent = 'img[width,height]';
  config.clipboard_handleImages = false;
  config.extraPlugins = 'fakeobjects, iframe, autogrow';

  config.iframe_attributes = {
    sandbox: 'allow-scripts allow-same-origin allow-popups allow-presentation',
    allowfullscreen: '',
    loading: 'lazy',
    referrerpolicy: 'no-referrer-when-downgrade'
  };

  config.toolbar = [
    { name: 'document', groups: [ 'mode', 'document', 'doctools' ], items: [ 'Source'] },
    { name: 'clipboard', groups: [ 'clipboard', 'undo' ], items: [ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ] },
    { name: 'links', items: [ 'Link', 'Unlink', 'Anchor' ] },
    { name: 'insert', items: [ 'Image', 'Flash', 'Table', 'HorizontalRule', 'SpecialChar', 'Iframe' ] },
    { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi' ], items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock' ] },
    '/',
    { name: 'styles', items: [ 'Styles', 'Format', 'Font', 'FontSize' ] },
    { name: 'colors', items: [ 'TextColor', 'BGColor' ] },
    { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ], items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ] }
  ];
}
```

### 拡張機能の追加
画像の挿入はデフォルトの機能で対応できますが、他にGoogleMapやYouTubeの埋め込みを行うため`<iframe>`タグの挿入もできるようにするという要件がありました。
デフォルトの機能では備わっていないため、`iframe`拡張機能を利用します。
拡張機能を利用するために、必要なファイル群を公式サイトからダウンロードします。例えばiframeの場合は以下のページからライブラリをダウンロードし、`vendor/assets/javascripts/ckeditor`配下に配置します。
https://ckeditor.com/cke4/addon/iframe

ここで、注意点。ダウンロードすると以下のようなダイアログが表示されます。この`Add-on dependencies`にあるライブラリもダウンロードして、置く必要がありそうです。
<img width="400" alt="iframe.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1697316/07f587d9-98c1-af84-159a-5efd550648b7.jpeg">

`iframe`の場合、`Dialog`と`Fake Objects`に依存していますが、どうやらDialogの方はifameのライブラリに含まれているのか、`Fake Objects`のみのダウンロードでいけました。その他に、エディタの高さを改行の数に応じて、自動で高さ調整してくれる`autogrow`も導入しています。
`config.js`の以下の部分が拡張ライブラリの設定部分です。

```JavaScript:vendor/assets/javascripts/ckeditor/config.js

CKEDITOR.plugins.addExternal( 'fakeobjects', '/assets/ckeditor/plugins/fakeobjects/', 'plugin.js' );
CKEDITOR.plugins.addExternal( 'iframe', '/assets/ckeditor/plugins/iframe/', 'plugin.js' );
CKEDITOR.plugins.addExternal( 'autogrow', '/assets/ckeditor/plugins/autogrow/', 'plugin.js' );

CKEDITOR.editorConfig = function (config) {
//・・・
  config.extraPlugins = 'fakeobjects, iframe, autogrow';
//・・・
}
```
これでGoogleMapやYouTubeの埋め込みなどを可能にするiframeタグの挿入ができるようになります。
<img width="400" alt="スクリーンショット 2023-08-13 18.04.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1697316/462e8d47-df6b-9202-ed41-f706e39fe87d.png">

## おわりに
最後までご覧いただきありがとうございました。
新しいバージョンのRailsを使用している場合は、Action Textを使用すると思いますが、古いバージョンのRailsを使っているプロジェクトの場合は外部JSライブラリから導入することになると思いますので、参考になれば幸いです。
CKEditor4系の使用感は悪くないです。今時の直感的な操作という訳にはいきませんが、拡張機能も豊富にありますし、CMS的なものには必要十分だと思います。


## 参考記事
[CKEditorをカスタマイズする](https://qiita.com/tanakaworld/items/a329f864e38fa0a3ca1d)