【Rails】メディアライブラリの画像をテキストエディタ内(TinyMCE)に挿入する方法

前回、僕の自作ブログにTinyMCEを利用して高機能エディタを導入しましたが、画像周りの実装はまだでした。

諸事情により自作ブログは閉鎖しております。

あわせて読みたい
【Rails】TinyMCE を使用してワードプレスのような高機能エディタを導入してみた つい最近、Railsで自作ブログを作って公開したのですが、ブログで重要な機能の一つ「エディタ」をどうしようか考えていたところ、 ワードプレスのような使い勝手の良い...

僕の理想としては、ワードプレスのような高機能エディタ(グーテンベルク以前のクラシックエディタ)を想定しておりまして、

画像はメディアライブラリ(フォトライブラリ)上から選択して、テキストエディタ上に挿入、プレビューできるようにしたいと思っていました。

そこで、今回は前回導入したTinyMCEに、「メディアライブラリから画像をテキストエディタ内に挿入できるようにする」という機能を追加したので、その手順をまとめました。

RailsでTinyMCEを導入する、TinyMCEで画像挿入できるようにする、を想定している方は参考にしてみてください。

(基本的に自分用のメモ書きとしてまとめているため、僕の記事見て分からなかったら他を当たってくださいw)

目次

開発環境

  • Ruby 3.1.2
  • Ruby on Rails 7.0.4.3
  • Bootstrap 5.2.3
  • M1 Macbook Air 2020
  • mac OS Monterey (ver. 12.4)

出来上がりイメージ

文字だけだとイメージしずらいと思うので、実際の動作イメージを以下に紹介します。

ワードプレス(もしくは他のブログサービスなど)でブログ書いたことのある方ならわかるかと思いますが、「メディアの追加」ボタンを押すとメディアライブラリ(フォトライブラリ)が表示され、

メディアライブラリ上にすでにアップロード済みの画像を、上記のように選択することでテキストエディタ上に挿入することができます。

今回、これらの一連の動作の中で「メディアライブラリで画像を選択→TInyMCEへの挿入」までを実装していきます。

メディアライブラリの画像をテキストエディタ上(TinyMCE)に挿入する

当記事ではTinyMCEを導入済み前提で話を進めていきます。

あわせて読みたい
【Rails】TinyMCE を使用してワードプレスのような高機能エディタを導入してみた つい最近、Railsで自作ブログを作って公開したのですが、ブログで重要な機能の一つ「エディタ」をどうしようか考えていたところ、 ワードプレスのような使い勝手の良い...

また、メディアライブラリの実装方法については、以下の過去記事に詳しくまとめましたので参考にしてみてください。(当記事ではメディアライブラリの実装方法については述べません)

あわせて読みたい
【Rails7】 Bootstrap と Stimulus でワードプレスのようなメディアライブラリを作ってみた(ActiveStor... 最近、Railsで自作ブログの運営を始めましたが、自作ブログを作る過程で悩んだのがメディア(画像)の扱い。 諸事情により自作ブログは閉鎖しております。 僕はワードプ...

(当記事は、上記記事の続編という位置付けになるかと思います。)

モーダルウィンドウに「選択」を追加

前回の記事で作成したモーダルウィンドウには画像の「選択」項目がなかったので、ここで「選択」を追加します。

<div data-controller="images">
  <%= form_with(model: post) do |form| %>
  
    <!-- メディアライブラリを開くボタン -->
    <div class="my-3">
      <div type="button" class="d-flex justify-content-center mx-1 mb-3 modal-btn" data-bs-toggle="modal" data-bs-target="#fileUploadModal"></div>
    </div>

    <!-- 省略 -->


  <% end %>

  <!-- メディア追加用のモーダルウィンドウ -->
  <div class="modal fade" id="fileUploadModal" tabindex="-1" aria-labelledby="fileUploadModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg modal-dialog-scrollable">
      <div class="modal-content">
        <div class="modal-header">
          <h1 class="modal-title fs-5" id="fileUploadModalLabel">メディアの選択またはアップロード</h1>
          <button type="button" class="btn-close" data-action="click->images#resetSelectedImage" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <!-- ドラッグ&ドロップ用のエリアに data-images-target="drop" と data-action を記述(アクションの中身は後ほど) -->
        <div class="modal-body" data-images-target="drop" data-action="dragover->images#dragover dragleave->images#dragleave drop->images#dropImages">
          <div class="my-3 text-center">
            <label for="filename">
              <!-- ボタンのCSSは省略。各自好きなデザインを -->
              <span class="upload-image-btn">ファイルを選択</span>
              <!-- 「ファイルを選択」ボタンに data-images-target="select" と data-action を記述(アクションの中身は後ほど) -->
              <input type="file" id="filename" accept="image/*" style="display: none" data-images-target="select" data-action="change->images#selectImages" multiple>
            </label>
          </div>
          <div class="my-3 text-center">
            <p>または</p>
            <p class="fs-5">ここにファイルをドラッグ&ドロップ</p>
          </div>
          <hr style="border: 1px solid #e3e1e1">
          <!-- 画像プレビューエリアに data-images-target="preview_images" と data-action を記述(アクションの中身は後ほど) -->
          <div class="my-2" data-images-target="preview_images">
            <!-- アップロード済みの画像をプレビュー(遅延読み込み) -->
            <% if current_user.images.exists? %>
              <%= turbo_frame_tag "images-page-#{@images.current_page}" do %>
                <% @images.each do |image| %>
                  <div class="image-box d-inline-flex justify-content-center mx-1 mb-3" data-action="click->images#selectedImageBox">
                    <%= image_tag(image, class: "mx-auto", id: image.blob_id) %>
                  </div>
                <% end %>
                <%= turbo_frame_tag "images-page-#{@images.next_page}", loading: :lazy, src: path_to_next_page(@images) %>
              <% end %>
            <% end %>
          </div>
        </div>
        <div class="modal-footer d-flex justify-content-start">
          <div>
            <button class="btn btn-danger" data-action="click->images#deleteImage">削除</button>
          </div>
          <!-- ↓「選択」ボタンを追加(images_controller.js 用の data-action も記述。内容は後ほど) -->
          <div>
            <button class="btn btn-primary" data-bs-dismiss="modal" data-action="click->images#insertImageTag click->images#resetSelectedImage">選択</button>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

images_controller.js を編集(imageタグを TinyMCEエディタ内に挿入するアクション)

$ rails g stimulusで作成したコントローラー(images_controller.js)に、以下のアクションを追記します。

  /********** エディタ(TinyMCE)にimageタグを挿入 ***********/
  insertImageTag() {
    const selectedImageBox = document.querySelector(".image-box-selected")
    if(selectedImageBox){
      const selectedImageTag = selectedImageBox.firstElementChild
      const sr = `${selectedImageTag.getAttribute("src")}`
      tinymce.activeEditor.insertContent('<img src="' + sr + '" class="image-size" />')
    }
  }

insertImageTag()アクション内の以下のコード↓

tinymce.activeEditor.insertContent('<img src="' + sr + '" class="image-size" />')

これを記述することで、TinyMCE内にimageタグを挿入し、かつ挿入したimageタグの画像をエディタ上で表示することができるようになります。(こちらの記事を参考にしました)

imageタグをTinyMCE内に挿入するとこんな感じになります↓

これでワードプレスのような画像挿入ができるようになりました。

ちなみに、images_controller.jsの全体のコードは以下のようになっています↓

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="images"
export default class extends Controller {
  static targets = ["select", "preview_images", "preview_thumbnail", "drop"]


  // ファイルサイズの上限を指定
  imageSizeOver(file){ // 1枚あたりのファイルサイズの上限(2MB)
    const fileSize = (file.size)/1000
    if(fileSize > 2000){
      return true
    }else{
      return false
    }
  }

  // 「ファイルを選択」ボタンを押下したときの動作(画像選択後→アップロード)
  selectImages(){
    const files = this.selectTargets[0].files
    // clearTimeout(this.timeoutImages)
    for(const file of files){
      if(this.imageSizeOver(file)){
        this.errorTarget.textContent = "ファイルサイズの上限(1枚あたり2MB)を超えている画像はアップロードできません。"
        this.timeoutImages = setTimeout(() =>{
          this.errorTarget.textContent = ""
        }, 3000)
      }else{
        this.uploadImage(file)
      }
    }
    this.selectTarget.value = "" // 選択中のファイルをリセット
  }

  // ドラッグオーバーしたときの動作
  dragover(e){
    e.preventDefault()
    this.dropTarget.classList.add("image-drag-over")
  }

  // ドラッグリーブしたときの動作
  dragleave(e){
    e.preventDefault()
    this.dropTarget.classList.remove("image-drag-over")
  }

  // ドロップしたときの動作(画像ドロップ後→アップロード)
  dropImages(e){
    e.preventDefault()
    this.dropTarget.classList.remove("image-drag-over")
    const files = e.dataTransfer.files
    for(const file of files){
      if(this.imageSizeOver(file)){
        this.errorTarget.textContent = "ファイルサイズの上限(1枚あたり2MB)を超えている画像はアップロードできません。"
        this.timeoutImages = setTimeout(() =>{
          this.errorTarget.textContent = ""
        }, 3000)
      }else{
        this.uploadImage(file)
      }
    }
  }

  // 画像アップロード用のアクション
  uploadImage(file){
    const csrfToken = document.getElementsByName('csrf-token')[0].content // CSRFトークンを取得
    const formData = new FormData()
    formData.append("file", file) // formDataオブジェクトに画像ファイルをセット
    const options = {
      method: 'POST',
      headers: {
        'X-CSRF-Token': csrfToken
      },
      body: formData
    }
    fetch("/images/upload_image", options)
      .then(response => response.json())
      .then(data => {
        this.previewImage(file, data.id, data.location)
      })
      .catch((error) => {
        console.error(error)
      })
  }

  // 画像プレビュー用のアクション
  previewImage(file, blob_id, blob_src){
    const previewImages = this.preview_imagesTarget
    const fileReader = new FileReader()
    const setAttr = (element, obj)=>{
      Object.keys(obj).forEach((key)=>{
        element.setAttribute(key, obj[key])
      })
    }
    fileReader.readAsDataURL(file)
    fileReader.onload = (function() {
      const img = new Image()
      const imgBox = document.createElement("div")
      const imgBoxAttr = {
        "class" : "image-box d-inline-flex justify-content-center mx-1 mb-3",
        "data-action" : "click->images#selectedImageBox"
      }
      setAttr(imgBox, imgBoxAttr)
      imgBox.appendChild(img)
      img.src = blob_src
      img.id = blob_id
      img.classList.add("mx-auto")
      console.log(img)
      previewImages.prepend(imgBox)
    })
  }

  // 画像を選択したときの動作(選択状態の時、サムネイルの周りが青く光る)
  selectedImageBox(e){
    const imageBoxSelected = e.currentTarget // 現在の要素を取得( e.currentTarget )
    const imageBoxes = document.querySelectorAll(".image-box-selected")
    for(const imageBox of imageBoxes){
      imageBox.classList.remove("image-box-selected") //image-box-selectedクラスを一旦削除(初期化)
    }
    imageBoxSelected.classList.add("image-box-selected") // 選択画像にimage-box-selectedクラスを追加
  }

  // 選択の解除
  resetSelectedImage(){
    const selectedImages = this.preview_imagesTarget.querySelectorAll(".image-box-selected")
    for(const selectedImage of selectedImages){
      selectedImage.classList.remove("image-box-selected")
    }
  }

  // 画像削除のアクション
  deleteImage(){
    const thisImageBox = document.querySelector(".image-box-selected")
    if(thisImageBox){
      const imageId = thisImageBox.firstElementChild.id
      const csrfToken = document.getElementsByName('csrf-token')[0].content // CSRFトークンを取得
      const formData = new FormData()
      formData.append("image_id", imageId)
      const options = {
        method: 'POST',
        headers: {
          'X-CSRF-Token': csrfToken
        },
        body: formData
      }
      const result = window.confirm("本当に削除しますか?")
      if(result){
        fetch("/images/delete_image", options)
        thisImageBox.remove()
      }
    }
  }

  /******************************** ↓追記 *********************************/
  /********** エディタ(TinyMCE)にimageタグを挿入 ***********/
  insertImageTag() {
    const selectedImageBox = document.querySelector(".image-box-selected")
    if(selectedImageBox){
      const selectedImageTag = selectedImageBox.firstElementChild
      const sr = `${selectedImageTag.getAttribute("src")}`
      tinymce.activeEditor.insertContent('<img src="' + sr + '" class="image-size" />')
    }
  }
  /***********************************************************************/


}

以上です。

参考資料

Stack Overflow
Insert an image into a tinymce editor I have a tinymce text box on my html page. Under this text box I have a table with my file system in which I can navigate to locate my images. I would like to b...
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

愛知の34歳。無職で暇になり始めたプログラミング(Ruby on Rails)の忘備録をまとめたブログです。最近は別にやりたいことができたのでプログラミングほぼやっていません。気が向いたらまた再開するかも。僕の日常はメインブログの方で更新しています。

コメント

コメントする

目次