How to display embed video with ActionText
Asked Answered
P

3

16

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.

Pul answered 26/5, 2019 at 19:1 Comment(7)
Your solution helped me a great deal with the direction. I started from there and then tweak around until I was able to get it working. Just don't have the time to post the whole solution here. Get in touch if you're still looking for a solution.Heidt
Embedding Youtube that works for me: blog.corsego.com/action-text-embed-youtubeBartels
@Bartels - why is the Embed controller not a Stimulus controller?Collop
@Collop because I didn't start using Stimulus JS before 2021 :)Bartels
@Bartels - makes sense. Its same in Chris Olivers git repo for that rails conf presentation too. Stimulus for mentions and plain js controller for embedsCollop
@Collop yeah, on the beginning of the post I mention the source ;)Bartels
@Bartels - totes. your answer was a big help thanks. I also subscribed to drifting ruby where he has a full rundown of this using stimulus.Collop
P
3

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
Porous answered 3/3, 2021 at 11:6 Comment(0)
Z
1

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.

Zircon answered 27/7, 2019 at 11:29 Comment(0)
B
0

I couldn't test your complex example, but if you want to put ActionText's HTML, it would be helpful.

Please try this:

<%= raw your_action_text_object.to_plain_text %>

Baccate answered 16/7, 2020 at 9:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.