ปรับแต่ง ActionText ให้รองรับ Youtube Embeded

ปกติแล้วการใช้งาน ActionText รองรับการใส่ข้อมูลที่เป็น link อยู่แล้วตั้งแต่ต้น แต่ในกรณีที่เราอยากใส่ Embeded Link อย่างเช่น Youtube หรือ Vimeo เข้าไปและอยากให้มีการแสดงภาพตัวอย่างของวิดีโอปรากฏขึ้นในตัว editor หรือจะแสดง Video Player ขึ้นมาเมื่ออยู่ในหน้าแสดงผล เราจะต้องมีการเพิ่มเติมความสามารถให้กับ ActionText กันเล็กน้อย

default actiontext Link in ActionText

รูปแบบในการพัฒนา

export default class YoutubeEmbedded {
  extend (parent) {
    const buttonHTML =
    '<button type="button" class="trix-button" data-trix-attribute="embed" data-trix-action="embed" title="Embed" tabindex="-1">Youtube Embed</button>'
    const buttonGroup = parent.toolbarElement.querySelector(
      ".trix-button-group--text-tools"
    )
    const dialogHml =
      `<div class="trix-dialog trix-dialog--link" data-trix-dialog="embed" data-trix-dialog-attribute="embed">
        <div class="trix-dialog__link-fields">
          <input type="text" name="embed" class="trix-input trix-input--dialog" placeholder="Paste your youtube video url" aria-label="embed code" required="" data-trix-input="" disabled="disabled">
          <div class="trix-button-group">
            <input type="button" class="trix-button trix-button--dialog" data-trix-custom="add-embed" value="Add">
          </div>
        </div>
      </div>`
    const dialogGroup = parent.toolbarElement.querySelector(".trix-dialogs")
    buttonGroup.insertAdjacentHTML("beforeend", buttonHTML)
    dialogGroup.insertAdjacentHTML("beforeend", dialogHml)

    this.addEmbedEventListener()
  }

  addEmbedEventListener () {
    document
      .querySelector('[data-trix-action="embed"]')
      .addEventListener("click", event => {
        const dialog = document.querySelector('[data-trix-dialog="embed"]');
        const embedInput = document.querySelector('[name="embed"]');
        if (event.target.classList.contains("trix-active")) {
          event.target.classList.remove("trix-active");
          dialog.classList.remove("trix-active");
          delete dialog.dataset.trixActive;
          embedInput.setAttribute("disabled", "disabled");
        } else {
          event.target.classList.add("trix-active");
          dialog.classList.add("trix-active");
          dialog.dataset.trixActive = "";
          embedInput.removeAttribute("disabled");
          embedInput.focus();
        }
      })

    document
      .querySelector('[data-trix-custom="add-embed"]')
      .addEventListener("click", event => {
        document.dispatchEvent(new CustomEvent("add-embed", {
          bubbles: true,
          detail: { link: () => document.querySelector('[name="embed"]').value }
        }))
      })
  }
}
import { Controller } from "stimulus"
import Trix from 'trix'
import YoutubeEmbedded from 'embed'
export default class extends Controller {
  static targets = [ "field" ]

  connect () {
    this.editor = this.fieldTarget.editor;
    let embed = new YoutubeEmbedded()
    embed.extend(this.fieldTarget)
  }
}
<div class="field">
  <%= form.label :content %>
  <%= form.rich_text_area :content, data: { controller: "embed", target: "embed.field" } %>
</div>

Embed Button ปุ่ม Embed

$ rails generate model Embed link:string link_id:string

app/models/embed.rb

class Embed < ApplicationRecord
  include ActionText::Attachable
end

app/javascript/controllers/embed_controller.js

  document.addEventListener("add-embed", (event) => {
    let link = event.detail.link()
    if (link) {
      const token = document.querySelector('meta[name="csrf-token"]').content
      fetch('/embeds', {
        method: "POST",
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': token
        },
        body: JSON.stringify({
          embeds: {
            link
          }
        })
      })
      .then(response => response.json())
      .then(({ sgid, content, contentType }) => {
        const attachment = new Trix.Attachment({
          content,
          sgid,
          contentType,
          filename: link,
          previewable: true
        })
        this.editor.insertAttachment(attachment)
        this.editor.insertLineBreak()
      })
    }
  })

app/controllers/embeds_controller.rb

  def create
    @embed = Embed.create(embed_params)
    respond_to do |format|
      format.json
    end
  end

app/views/embeds/create.json.jbuilder

json.extract! @embed, :link
json.sgid @embed.attachable_sgid
json.content render(partial: "embed/thumbnail", locals: { embed: @embed }, formats: [:html])
json.contentType "embed/youtube-video"

app/views/embeds/_thumbnail.html.erb

<%= image_tag "http://i3.ytimg.com/vi/#{embed.link_id}/maxresdefault.jpg", size: 300 %>

app/models/embed.rb

  def to_trix_content_attachment_partial_path
    'embed/thumbnail'
  end

Insert Youtube Link ใส่ลิงค์ของ Youtube เข้าไป

Preview แสดงภาพตัวอย่างของวิดีโอที่ใส่เข้าไป

app/views/embeds/_embed.html.erb

<div class="embed-responsive">
  <iframe width="640" height="360" src="https://www.youtube.com/embed/<%= embed.link_id %>" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>

config/application.rb

  config.to_prepare do
    ActionText::ContentHelper.allowed_tags << "iframe"
  end

Show Player แสดงวิดีโอ

app/javascript/packs/application.js

document.addEventListener("turbolinks:load", () => {
  let elements = document.querySelectorAll("action-text-attachment[content-type^=embed]")
  elements.forEach(element => {
    let caption = element.getAttribute('caption')
    if (caption != null)
      element.insertAdjacentHTML('beforeend', `<caption>${caption}</caption>`)
  })
})

Show Player แสดงวิดีโอและ caption

References