trix_embed 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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