trix_embed 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +94 -0
- data/app/assets/builds/trix-embed.js +22 -0
- data/app/assets/builds/trix-embed.metafile.json +1 -0
- data/app/helpers/trix_embed/application_helper.rb +11 -0
- data/app/javascript/controller.js +254 -0
- data/app/javascript/encryption.js +119 -0
- data/app/javascript/guard.js +64 -0
- data/app/javascript/index.js +22 -0
- data/app/javascript/media.js +102 -0
- data/app/javascript/renderer.js +127 -0
- data/app/javascript/store.js +35 -0
- data/app/javascript/templates.js +36 -0
- data/app/javascript/urls.js +97 -0
- data/app/models/trix_embed/attachment.rb +94 -0
- data/app/views/action_text/contents/_content.html.erb +1 -0
- data/app/views/trix_embed/_action_text_attachment.html.erb +3 -0
- data/app/views/trix_embed/_action_text_content_edit.html.erb +1 -0
- data/app/views/trix_embed/_action_text_content_show.html.erb +3 -0
- data/lib/trix_embed/engine.rb +26 -0
- data/lib/trix_embed/version.rb +5 -0
- data/lib/trix_embed.rb +3 -0
- metadata +362 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
import { createURL } from './urls'
|
2
|
+
|
3
|
+
const audioMediaTypes = {
|
4
|
+
mp3: 'audio/mpeg', // MP3 audio format
|
5
|
+
ogg: 'audio/ogg', // OGG audio format
|
6
|
+
wav: 'audio/wav' // WAV audio format
|
7
|
+
}
|
8
|
+
|
9
|
+
const imageMediaTypes = {
|
10
|
+
avif: 'image/avif', // AVIF image format
|
11
|
+
bmp: 'image/bmp', // BMP image format
|
12
|
+
gif: 'image/gif', // GIF image format
|
13
|
+
heic: 'image/heic', // HEIC image format
|
14
|
+
heif: 'image/heif', // HEIF image format
|
15
|
+
ico: 'image/x-icon', // ICO image format
|
16
|
+
jp2: 'image/jp2', // JPEG 2000 image format
|
17
|
+
jpeg: 'image/jpeg', // JPEG image format
|
18
|
+
jpg: 'image/jpeg', // JPEG image format (alternative extension)
|
19
|
+
jxr: 'image/vnd.ms-photo', // JPEG XR image format
|
20
|
+
png: 'image/png', // PNG image format
|
21
|
+
svg: 'image/svg+xml', // SVG image format
|
22
|
+
tif: 'image/tiff', // TIFF image format
|
23
|
+
tiff: 'image/tiff', // TIFF image format (alternative extension)
|
24
|
+
webp: 'image/webp' // WebP image format
|
25
|
+
}
|
26
|
+
|
27
|
+
const videoMediaTypes = {
|
28
|
+
mp4: 'video/mp4', // MP4 video format
|
29
|
+
ogv: 'video/ogg', // OGG video format
|
30
|
+
webm: 'video/webm' // WebM video format
|
31
|
+
}
|
32
|
+
|
33
|
+
// TODO: Expand to all media types once proper templates are implemented
|
34
|
+
const mediaTypes = imageMediaTypes
|
35
|
+
|
36
|
+
const tagsWithHrefAttribute = [
|
37
|
+
'animate', // SVG: Animation
|
38
|
+
'animateMotion', // SVG: Animation
|
39
|
+
'animateTransform', // SVG: Animation
|
40
|
+
'area', // HTML: Image map area
|
41
|
+
'audio', // HTML: Audio content
|
42
|
+
'base', // HTML: Base URL
|
43
|
+
'embed', // HTML: Embedded content
|
44
|
+
'feDisplacementMap', // SVG: Filter primitive
|
45
|
+
'feImage', // SVG: Filter primitive
|
46
|
+
'feTile', // SVG: Filter primitive
|
47
|
+
'filter', // SVG: Filter container
|
48
|
+
'font-face-uri', // SVG: Font reference
|
49
|
+
'iframe', // HTML: Inline frame
|
50
|
+
'image', // SVG: Image
|
51
|
+
'link', // HTML: External resources (e.g., stylesheets)
|
52
|
+
'object', // HTML: Embedded content (fallback for non-HTML5 browsers)
|
53
|
+
'script', // HTML: External scripts
|
54
|
+
'source', // HTML: Media source
|
55
|
+
'track', // HTML: Text tracks for media elements
|
56
|
+
'use', // SVG: Reuse shapes from other documents
|
57
|
+
'video' // HTML: Video content
|
58
|
+
]
|
59
|
+
|
60
|
+
const tagsWithSrcAttribute = [
|
61
|
+
'audio', // HTML: Audio content
|
62
|
+
'embed', // HTML: Embedded content
|
63
|
+
'iframe', // HTML: Inline frame
|
64
|
+
'img', // HTML: Images
|
65
|
+
'input', // HTML: Input elements with type="image"
|
66
|
+
'script', // HTML: External scripts
|
67
|
+
'source', // HTML: Media source
|
68
|
+
'track', // HTML: Text tracks for media elements
|
69
|
+
'video', // HTML: Video content
|
70
|
+
'frame', // HTML: Deprecated (use iframe instead)
|
71
|
+
'frameset', // HTML: Deprecated (use iframe instead)
|
72
|
+
'object', // HTML: Embedded content
|
73
|
+
'picture', // HTML: Responsive images
|
74
|
+
'use' // SVG: Reuse shapes from other documents
|
75
|
+
]
|
76
|
+
|
77
|
+
export const mediaTags = tagsWithHrefAttribute.concat(tagsWithSrcAttribute)
|
78
|
+
|
79
|
+
export function isAudio(url) {
|
80
|
+
return !!Object.values(audioMediaTypes).find(t => t === getMediaType(url))
|
81
|
+
}
|
82
|
+
|
83
|
+
export function isImage(url) {
|
84
|
+
return !!Object.values(imageMediaTypes).find(t => t === getMediaType(url))
|
85
|
+
}
|
86
|
+
|
87
|
+
export function isVideo(url) {
|
88
|
+
return !!Object.values(videoMediaTypes).find(t => t === getMediaType(url))
|
89
|
+
}
|
90
|
+
|
91
|
+
export function getMediaType(value) {
|
92
|
+
let url
|
93
|
+
|
94
|
+
createURL(value, u => (url = u))
|
95
|
+
if (!url) return null
|
96
|
+
|
97
|
+
const index = url.pathname.lastIndexOf('.')
|
98
|
+
if (!index) return null
|
99
|
+
|
100
|
+
const extension = url.pathname.substring(index + 1)
|
101
|
+
return mediaTypes[extension]
|
102
|
+
}
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import { createURL, extractURLHosts } from './urls'
|
2
|
+
import { isImage, getMediaType } from './media'
|
3
|
+
import { getTemplate } from './templates'
|
4
|
+
|
5
|
+
export default class Renderer {
|
6
|
+
// Constructs a new Renderer instance
|
7
|
+
//
|
8
|
+
// @param {Controller} controller - a Stimulus Controller instance
|
9
|
+
constructor(controller) {
|
10
|
+
this.controller = controller
|
11
|
+
this.initializeTempates()
|
12
|
+
}
|
13
|
+
|
14
|
+
initializeTempates() {
|
15
|
+
const templates = ['error', 'exception', 'header', 'iframe', 'image']
|
16
|
+
templates.forEach(name => this.initializeTemplate(name))
|
17
|
+
}
|
18
|
+
|
19
|
+
initializeTemplate(name) {
|
20
|
+
let template
|
21
|
+
|
22
|
+
if (this.controller[`has${name.charAt(0).toUpperCase() + name.slice(1)}TemplateValue`])
|
23
|
+
template = document.getElementById(this.controller[`${name}TemplateValue`])
|
24
|
+
|
25
|
+
this[`${name}Template`] = template || getTemplate(name)
|
26
|
+
}
|
27
|
+
|
28
|
+
// Renders an embed header
|
29
|
+
//
|
30
|
+
// @param {String} html - HTML
|
31
|
+
// @returns {String} HTML
|
32
|
+
//
|
33
|
+
renderHeader(html) {
|
34
|
+
const header = this.headerTemplate.content.firstElementChild.cloneNode(true)
|
35
|
+
const h1 = header.tagName.match(/h1/i) ? header : header.querySelector('h1')
|
36
|
+
h1.innerHTML = html
|
37
|
+
return header.outerHTML
|
38
|
+
}
|
39
|
+
|
40
|
+
// TODO: Add templates for links
|
41
|
+
// Renders a list of URLs as a list of HTML links i.e. anchor tags <a>
|
42
|
+
//
|
43
|
+
// @param {String[]} urls - list of URLs
|
44
|
+
// @returns {String[]} list of individual HTML links
|
45
|
+
//
|
46
|
+
renderLinks(urls = ['https://example.com', 'https://test.com']) {
|
47
|
+
urls = urls
|
48
|
+
.filter(url => {
|
49
|
+
let ok = false
|
50
|
+
createURL(url, u => (ok = true))
|
51
|
+
return ok
|
52
|
+
})
|
53
|
+
.sort()
|
54
|
+
|
55
|
+
if (!urls.length) return
|
56
|
+
const links = urls.map(url => `<li><a href='${url}'>${url}</a></li>`)
|
57
|
+
return `<ul>${links.join('')}</ul><br>`
|
58
|
+
}
|
59
|
+
|
60
|
+
// TOOO: add support for audio and video
|
61
|
+
// Renders a URL as an HTML embed i.e. an iframe or media tag (img, video, audio etc.)
|
62
|
+
//
|
63
|
+
// @param {String} url - URL
|
64
|
+
// @returns {String} HTML
|
65
|
+
//
|
66
|
+
renderEmbed(url = 'https://example.com') {
|
67
|
+
let embed
|
68
|
+
|
69
|
+
if (isImage(url)) {
|
70
|
+
embed = this.imageTemplate.content.firstElementChild.cloneNode(true)
|
71
|
+
const img = embed.tagName.match(/img/i) ? embed : embed.querySelector('img')
|
72
|
+
img.src = url
|
73
|
+
} else {
|
74
|
+
embed = this.iframeTemplate.content.firstElementChild.cloneNode(true)
|
75
|
+
const iframe = embed.tagName.match(/iframe/i) ? embed : embed.querySelector('iframe')
|
76
|
+
iframe.src = url
|
77
|
+
}
|
78
|
+
|
79
|
+
return embed.outerHTML
|
80
|
+
}
|
81
|
+
|
82
|
+
// Renders a list of URLs as HTML embeds i.e. iframes or media tags (img, video, audio etc.)
|
83
|
+
//
|
84
|
+
// @param {String[]} urls - list of URLs
|
85
|
+
// @returns {String[]} list of individual HTML embeds
|
86
|
+
//
|
87
|
+
renderEmbeds(urls = ['https://example.com', 'https://test.com']) {
|
88
|
+
if (!urls?.length) return
|
89
|
+
return urls.map(url => this.renderEmbed(url))
|
90
|
+
}
|
91
|
+
|
92
|
+
// Renders embed errors
|
93
|
+
//
|
94
|
+
// @param {String[]} urls - list of URLs
|
95
|
+
// @param {String[]} allowedHosts - list of allowed hosts
|
96
|
+
// @returns {String} HTML
|
97
|
+
//
|
98
|
+
renderErrors(urls = ['https://example.com', 'https://test.com'], allowedHosts = []) {
|
99
|
+
if (!urls?.length) return
|
100
|
+
|
101
|
+
const element = this.errorTemplate.content.firstElementChild.cloneNode(true)
|
102
|
+
const prohibitedHostsElement = element.querySelector('[data-list="prohibited-hosts"]')
|
103
|
+
const allowedHostsElement = element.querySelector('[data-list="allowed-hosts"]')
|
104
|
+
|
105
|
+
if (prohibitedHostsElement) {
|
106
|
+
const hosts = extractURLHosts(urls).sort()
|
107
|
+
if (hosts.length) prohibitedHostsElement.innerHTML = hosts.map(host => `<li>${host}</li>`).join('')
|
108
|
+
}
|
109
|
+
|
110
|
+
if (allowedHostsElement && allowedHosts.length)
|
111
|
+
allowedHostsElement.innerHTML = allowedHosts.map(host => `<li>${host}</li>`).join('')
|
112
|
+
|
113
|
+
return element.outerHTML
|
114
|
+
}
|
115
|
+
|
116
|
+
// Renders an exception
|
117
|
+
//
|
118
|
+
// @param {String[]} ex - The exception
|
119
|
+
// @returns {String} HTML
|
120
|
+
//
|
121
|
+
renderException(ex) {
|
122
|
+
const element = this.exceptionTemplate.content.firstElementChild.cloneNode(true)
|
123
|
+
const code = element.querySelector('code')
|
124
|
+
code.innerHTML = ex.message
|
125
|
+
return element.outerHTML
|
126
|
+
}
|
127
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
export default class Store {
|
2
|
+
constructor(controller) {
|
3
|
+
this.controller = controller
|
4
|
+
this.base = this.obfuscate([location.pathname, this.controller.element.closest('[id]')?.id].join('/'))
|
5
|
+
}
|
6
|
+
|
7
|
+
split(list) {
|
8
|
+
const index = Math.ceil(list.length / 2)
|
9
|
+
return [list.slice(0, index), list.slice(index)]
|
10
|
+
}
|
11
|
+
|
12
|
+
obfuscate(value) {
|
13
|
+
const chars = [...value].map(char => char.charCodeAt(0))
|
14
|
+
const parts = this.split(chars)
|
15
|
+
return [parts[1]?.reverse(), chars[0]].flat().join('')
|
16
|
+
}
|
17
|
+
|
18
|
+
read(key) {
|
19
|
+
return sessionStorage.getItem(this.generateStorageKey(key))
|
20
|
+
}
|
21
|
+
|
22
|
+
write(key, value) {
|
23
|
+
return sessionStorage.setItem(this.generateStorageKey(key), value)
|
24
|
+
}
|
25
|
+
|
26
|
+
remove(key) {
|
27
|
+
return sessionStorage.removeItem(this.generateStorageKey(key))
|
28
|
+
}
|
29
|
+
|
30
|
+
generateStorageKey(value) {
|
31
|
+
const chars = [...this.obfuscate(value)]
|
32
|
+
const [prefix, suffix] = this.split(chars)
|
33
|
+
return btoa(`${prefix}/${this.base}/${suffix}`)
|
34
|
+
}
|
35
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
const defaults = {
|
2
|
+
header: `<h1></h1>`,
|
3
|
+
iframe: `<iframe></iframe>`,
|
4
|
+
image: `<img></img>`,
|
5
|
+
|
6
|
+
error: `
|
7
|
+
<div>
|
8
|
+
<h1>Copy/Paste Info</h1>
|
9
|
+
<h3>The pasted content includes media from unsupported hosts.</h3>
|
10
|
+
|
11
|
+
<h2>Prohibited Hosts / Domains</h2>
|
12
|
+
<ul data-list="prohibited-hosts">
|
13
|
+
<li>Media is only supported from allowed hosts.</li>
|
14
|
+
</ul>
|
15
|
+
|
16
|
+
<h2>Allowed Hosts / Domains</h2>
|
17
|
+
<ul data-list="allowed-hosts">
|
18
|
+
<li>Allowed hosts not configured.</li>
|
19
|
+
</ul>
|
20
|
+
</div>
|
21
|
+
`,
|
22
|
+
|
23
|
+
exception: `
|
24
|
+
<div style='background-color:lightyellow; color:red; border:solid 1px red; padding:20px;'>
|
25
|
+
<h1>Unhandled Exception!</h1>
|
26
|
+
<p>Show a programmer the message below.</p>
|
27
|
+
<pre style="background-color:darkslategray; color:whitesmoke; padding:10px;"><code></code></pre>
|
28
|
+
</div>
|
29
|
+
`
|
30
|
+
}
|
31
|
+
|
32
|
+
export function getTemplate(name) {
|
33
|
+
const template = document.createElement('template')
|
34
|
+
template.innerHTML = defaults[name]
|
35
|
+
return template
|
36
|
+
}
|
@@ -0,0 +1,97 @@
|
|
1
|
+
// Creates a URL object from a value and yields the result
|
2
|
+
//
|
3
|
+
// @param {String} value - Value to convert to a URL (coerced to a string)
|
4
|
+
// @param {Function} callback - Function to be called with the URL object
|
5
|
+
// @returns {URL, null} URL object
|
6
|
+
//
|
7
|
+
export function createURL(value, callback = url => {}) {
|
8
|
+
try {
|
9
|
+
const url = new URL(String(value).trim())
|
10
|
+
if (callback) callback(url)
|
11
|
+
return url
|
12
|
+
} catch (_error) {
|
13
|
+
console.info(`Failed to parse URL! value='${value}']`)
|
14
|
+
}
|
15
|
+
return null
|
16
|
+
}
|
17
|
+
|
18
|
+
// Creates a URL host from a value and yields the result
|
19
|
+
//
|
20
|
+
// @param {String} value - Value to convert to a URL host (coerced to a string)
|
21
|
+
// @param {Function} callback - Function to be called with the URL host
|
22
|
+
// @returns {String, null} URL host
|
23
|
+
//
|
24
|
+
function createURLHost(value, callback = host => {}) {
|
25
|
+
let host = null
|
26
|
+
createURL(value, url => (host = url.host))
|
27
|
+
if (host && callback) callback(host)
|
28
|
+
return host
|
29
|
+
}
|
30
|
+
|
31
|
+
function extractURLsFromTextNodes(element) {
|
32
|
+
const urls = []
|
33
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, node => {
|
34
|
+
const value = node.nodeValue
|
35
|
+
if (!value.includes('http')) return NodeFilter.FILTER_REJECT
|
36
|
+
return NodeFilter.FILTER_ACCEPT
|
37
|
+
})
|
38
|
+
|
39
|
+
let node
|
40
|
+
while ((node = walker.nextNode()))
|
41
|
+
node.nodeValue
|
42
|
+
.split(/\s+/)
|
43
|
+
.filter(val => val.startsWith('http'))
|
44
|
+
.forEach(match =>
|
45
|
+
createURL(match, url => {
|
46
|
+
if (!urls.includes(url.href)) urls.push(url.href)
|
47
|
+
})
|
48
|
+
)
|
49
|
+
|
50
|
+
return urls
|
51
|
+
}
|
52
|
+
|
53
|
+
function extractURLsFromElements(element) {
|
54
|
+
const urls = []
|
55
|
+
|
56
|
+
if (element.src) createURL(element.src, url => urls.push(url.href))
|
57
|
+
if (element.href)
|
58
|
+
createURL(element.href, url => {
|
59
|
+
if (!urls.includes(url.href)) urls.push(url.href)
|
60
|
+
})
|
61
|
+
|
62
|
+
const elements = element.querySelectorAll('[src], [href]')
|
63
|
+
elements.forEach(el => {
|
64
|
+
createURL(el.src || el.href, url => {
|
65
|
+
if (!urls.includes(url.href)) urls.push(url.href)
|
66
|
+
})
|
67
|
+
})
|
68
|
+
|
69
|
+
return urls
|
70
|
+
}
|
71
|
+
|
72
|
+
export function validateURL(value, allowedHosts = []) {
|
73
|
+
let valid = false
|
74
|
+
createURLHost(value, host => (valid = !!allowedHosts.find(allowedHost => host.includes(allowedHost))))
|
75
|
+
return valid
|
76
|
+
}
|
77
|
+
|
78
|
+
export function extractURLHosts(values) {
|
79
|
+
return values.reduce((hosts, value) => {
|
80
|
+
createURLHost(value, host => {
|
81
|
+
if (!hosts.includes(host)) hosts.push(host)
|
82
|
+
})
|
83
|
+
return hosts
|
84
|
+
}, [])
|
85
|
+
}
|
86
|
+
|
87
|
+
// Extracts all URLs from an HTML element (all inclusive i.e. elements and text nodes)
|
88
|
+
//
|
89
|
+
// @param {HTMLElement} element - HTML element
|
90
|
+
// @returns {String[]} list of unique URLs
|
91
|
+
//
|
92
|
+
export function extractURLsFromElement(element) {
|
93
|
+
const elementURLs = extractURLsFromElements(element)
|
94
|
+
const textNodeURLs = extractURLsFromTextNodes(element)
|
95
|
+
const uniqueURLs = new Set([...elementURLs, ...textNodeURLs])
|
96
|
+
return [...uniqueURLs]
|
97
|
+
}
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TrixEmbed
|
4
|
+
class Attachment
|
5
|
+
include ActiveModel::Model
|
6
|
+
include GlobalID::Identification
|
7
|
+
include ActionText::Attachable
|
8
|
+
|
9
|
+
CONTENT_TYPE = "application/vnd.trix-embed"
|
10
|
+
ALLOWED_TAGS = ActionText::ContentHelper.allowed_tags + %w[iframe]
|
11
|
+
ALLOWED_ATTRIBUTES = ActionText::ContentHelper.allowed_attributes + %w[allow allowfullscreen allowpaymentrequest credentialless csp loading referrerpolicy sandbox srcdoc]
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def rewrite_action_text_content(content)
|
15
|
+
fragment = Nokogiri::HTML.fragment(content)
|
16
|
+
matches = fragment.css("#{ActionText::Attachment.tag_name}[sgid][content-type='#{CONTENT_TYPE}']")
|
17
|
+
|
18
|
+
matches.each do |match|
|
19
|
+
attachment = ActionText::Attachment.from_node(match)
|
20
|
+
attachable = attachment.attachable
|
21
|
+
|
22
|
+
html = ActionText::Content.render(
|
23
|
+
partial: attachable.to_action_text_content_partial_path,
|
24
|
+
locals: {attachable: attachable}
|
25
|
+
)
|
26
|
+
|
27
|
+
html = ActionText::ContentHelper.sanitizer.sanitize(
|
28
|
+
html,
|
29
|
+
tags: ALLOWED_TAGS,
|
30
|
+
attributes: ALLOWED_ATTRIBUTES,
|
31
|
+
scrubber: nil
|
32
|
+
)
|
33
|
+
|
34
|
+
match.replace html
|
35
|
+
end
|
36
|
+
|
37
|
+
fragment.to_html.html_safe
|
38
|
+
end
|
39
|
+
|
40
|
+
def rewrite_trix_html(trix_html)
|
41
|
+
fragment = Nokogiri::HTML.fragment(trix_html)
|
42
|
+
matches = fragment.css("[data-trix-attachment][data-trix-content-type='#{CONTENT_TYPE}']")
|
43
|
+
|
44
|
+
matches.each do |match|
|
45
|
+
data = JSON.parse(match["data-trix-attachment"]).deep_transform_keys(&:underscore)
|
46
|
+
|
47
|
+
attachable = TrixEmbed::Attachment.new(data)
|
48
|
+
attachment = ActionText::Attachment.from_attachable(attachable)
|
49
|
+
|
50
|
+
match.replace ActionText::Content.render(
|
51
|
+
partial: attachable.to_partial_path,
|
52
|
+
locals: {attachment: attachment}
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
fragment.to_html
|
57
|
+
end
|
58
|
+
|
59
|
+
def find(id)
|
60
|
+
new JSON.parse(id)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
attr_reader :attributes
|
65
|
+
attr_accessor :content_type, :content
|
66
|
+
|
67
|
+
def initialize(attributes = {})
|
68
|
+
super @attributes = attributes.with_indifferent_access.slice(:content_type, :content)
|
69
|
+
end
|
70
|
+
|
71
|
+
def id
|
72
|
+
attributes.to_json
|
73
|
+
end
|
74
|
+
|
75
|
+
def persisted?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
# What gets saved to the database
|
80
|
+
def to_partial_path
|
81
|
+
"trix_embed/action_text_attachment"
|
82
|
+
end
|
83
|
+
|
84
|
+
# What gets presented in the browser (show view)
|
85
|
+
def to_action_text_content_partial_path
|
86
|
+
"trix_embed/action_text_content_show"
|
87
|
+
end
|
88
|
+
|
89
|
+
# What gets presented in the browser (edit view)
|
90
|
+
def to_trix_content_attachment_partial_path
|
91
|
+
"trix_embed/action_text_content_edit"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= TrixEmbed::Attachment.rewrite_action_text_content render_action_text_content(content) %>
|
@@ -0,0 +1 @@
|
|
1
|
+
<%= trix_embed_attachment(local_assigns)&.content&.html_safe %>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "action_text"
|
4
|
+
require "active_model"
|
5
|
+
require "active_support/all"
|
6
|
+
require "globalid"
|
7
|
+
require_relative "version"
|
8
|
+
|
9
|
+
module TrixEmbed
|
10
|
+
def self.config
|
11
|
+
Rails.application.config.trix_embed
|
12
|
+
end
|
13
|
+
|
14
|
+
class Engine < ::Rails::Engine
|
15
|
+
isolate_namespace TrixEmbed
|
16
|
+
config.trix_embed = ActiveSupport::OrderedOptions.new
|
17
|
+
|
18
|
+
initializer "trix_embed.configuration" do
|
19
|
+
Mime::Type.register "application/vnd.trix-embed", :trix_embed
|
20
|
+
|
21
|
+
ActiveSupport.on_load :action_controller do
|
22
|
+
helper TrixEmbed::ApplicationHelper
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/trix_embed.rb
ADDED