Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to display embed video with ActionText

I am trying to display embedded videos with ActionText on Rails 6, both in the WYSIWYG Trix, and in the rendered content. But the ActionText renderer filters all raw html code and forces me to use JS to display the iframes in the rendered content, which doesnt work in Trix.

I followed the instructions given here by one of Basecamp's dev : https://github.com/rails/actiontext/issues/37#issuecomment-451627370. Step 1 through 3 work, but when ActionText renders my partial it filters the iframe.

The form creating the WYSIYWG

= form_for(article, url: url, method: method) do |a|
  = a.label :content
  = a.rich_text_area :content, data: { controller: "articles", target: "articles.field", embeds_path: editorial_publication_embeds_path(@publication, format: :json) }
  = a.submit submit_text, class:"btn full"

The Stimulus controller adding the embed functionality (in dire need of a refactor)

import { Controller } from "stimulus";
import Trix from "trix";

$.ajaxSetup({
  headers: {
    "X-CSRF-Token": $('meta[name="csrf-token"]').attr("content"),
  },
});

export default class extends Controller {
  static targets = ["field"];

  connect() {
    this.editor = this.fieldTarget.editor; 

    const buttonHTML =
      '<button type="button" class="trix-button" data-trix-attribute="embed" data-trix-action="embed" title="Embed" tabindex="-1">Media</button>';
    const buttonGroup = this.fieldTarget.toolbarElement.querySelector(
      ".trix-button-group--block-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 video or sound 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 = this.fieldTarget.toolbarElement.querySelector(
      ".trix-dialogs"
    );
    buttonGroup.insertAdjacentHTML("beforeend", buttonHTML);
    dialogGroup.insertAdjacentHTML("beforeend", dialogHml);
    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 => {
        const content = document.querySelector('[name="embed"]').value;
        if (content) {
          $.ajax({
            method: "POST",
            url: document.querySelector("[data-embeds-path]").dataset
              .embedsPath,
            data: {
              embed: {
                content,
              },
            },
            success: ({ content, sgid }) => {
              const attachment = new Trix.Attachment({
                content,
                sgid,
              });
              this.editor.insertAttachment(attachment);
              this.editor.insertLineBreak();
            },
          });
        }
      });
  }
}

The Embed model

class Embed < ApplicationRecord
  include ActionText::Attachable

  validates :content, presence: true

  after_validation :fetch_oembed_data

  def to_partial_path
    "editorial/embeds/embed"
  end

  def fetch_oembed_data
    url =
      case content
      when /youtube/
        "https://www.youtube.com/oembed?url=#{content}&format=json"
      when /soundcloud/
        "https://soundcloud.com/oembed?url=#{content}&format=json"
      when /twitter/
        "https://publish.twitter.com/oembed?url=#{content}"
      end
    res = RestClient.get url
    json = JSON.parse(res.body, object_class: OpenStruct)
    self.height = json.height
    self.author_url = json.author_url
    self.thumbnail_url = json.thumbnail_url
    self.width = json.width
    self.author_name = json.author_name
    self.thumbnail_height = json.thumbnail_height
    self.title = json.title
    self.version = json.version
    self.provider_url = json.provider_url
    self.thumbnail_width = json.thumbnail_width
    self.embed_type = json.type
    self.provider_name = json.provider_name
    self.html = json.html
  end
end

The controller creating the Embed

  def create
    @embed = Embed.create!(params.require(:embed).permit(:content))
    respond_to do |format|
      format.json
    end
  end

The jbuilder view responding to the ajax call to create the Embed

json.extract! @embed, :content

json.sgid @embed.attachable_sgid
json.content render(partial: "editorial/embeds/embed", locals: { embed: @embed }, formats: [:html])

The Embed HTML partial (slim)

.youtube-embed.embed
  .content
    = image_tag(embed.thumbnail_url) if embed.thumbnail_url.present?
    p = "Embed from #{embed.provider_name} (#{embed.content})"
    p.embed-html = embed.html

And finally the JS code displaying the iframes when the Article's content with Embeds inside is displayed

$(document).ready(() => {
  $(".embed").each(function(i, embed) {
    const $embed = $(embed);
    const p = $embed
      .find(".content")
      .replaceWith($embed.find(".embed-html").text());
  });
});

If I change the Embed partial to

== embed.html

It displays properly in the WYSIWYG but not in the rendered view.

like image 204
balthazar Avatar asked May 26 '19 19:05

balthazar


2 Answers

You need to add iframe to allowed_tags, add the following code in application.rb:

config.to_prepare do
  ActionText::ContentHelper.allowed_tags << "iframe"
end
like image 52
Yi Feng Xie Avatar answered Sep 23 '22 02:09

Yi Feng Xie


It looks like you need to whitelist the script that generates the iframe.

A quick test you can do is on the show page add the relevant JS for the content providers (I was testing Instagram attachments, so added <script async src="//www.instagram.com/embed.js"></script>).

It would be unwise to whitelist all <script> tags in ActionText views, but you can manage the script loading yourself.

like image 24
Toby Avatar answered Sep 23 '22 02:09

Toby