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.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With