trix_embed 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +120 -47
- data/app/assets/builds/trix-embed.js +38 -19
- data/app/assets/builds/trix-embed.metafile.json +1 -1
- data/app/javascript/controller.js +342 -145
- data/app/javascript/enumerable.js +19 -0
- data/app/javascript/forms.js +134 -0
- data/app/javascript/guard.js +29 -47
- data/app/javascript/index.js +9 -3
- data/app/javascript/media.js +12 -2
- data/app/javascript/metadata.js +4 -0
- data/app/javascript/renderer.js +155 -75
- data/app/javascript/store.js +13 -1
- data/app/javascript/templates.js +45 -28
- data/app/javascript/urls.js +57 -42
- data/app/models/trix_embed/attachment.rb +20 -6
- data/app/views/action_text/contents/_content.html.erb +1 -1
- data/lib/trix_embed/engine.rb +1 -1
- data/lib/trix_embed/version.rb +1 -1
- metadata +37 -4
@@ -0,0 +1,134 @@
|
|
1
|
+
import { trixEditorTagName } from './media'
|
2
|
+
import { createURLObject } from './urls'
|
3
|
+
|
4
|
+
let _patch
|
5
|
+
let _observer
|
6
|
+
|
7
|
+
const protectedForms = new Set()
|
8
|
+
const trixEmbedSelector = `${trixEditorTagName}[data-controller~="trix-embed"]`
|
9
|
+
|
10
|
+
function makeKey(form) {
|
11
|
+
let { method, action } = form || {}
|
12
|
+
action = createURLObject(action)?.pathname || action
|
13
|
+
return `${method}:${action}`.trim().toLowerCase()
|
14
|
+
}
|
15
|
+
|
16
|
+
function protect(form, input) {
|
17
|
+
if (!form) return
|
18
|
+
const key = makeKey(form)
|
19
|
+
protectedForms.add({ key, form, input })
|
20
|
+
}
|
21
|
+
|
22
|
+
function shouldSubmit(form) {
|
23
|
+
const key = makeKey(form)
|
24
|
+
const list = [...protectedForms].filter(f => f.key === key)
|
25
|
+
|
26
|
+
// form is not protected
|
27
|
+
if (!list.length) return true
|
28
|
+
|
29
|
+
// form contains a trix-embed and it's currently pasting
|
30
|
+
if (form.trixEmbedPasting) return false
|
31
|
+
|
32
|
+
// form contains a trix-embed and it's not currently pasting
|
33
|
+
if (form.querySelector(trixEmbedSelector)) return true
|
34
|
+
|
35
|
+
// form is protected but does not contain a trix-embed
|
36
|
+
// prevent submit if any of the following conditions are true
|
37
|
+
//
|
38
|
+
// - the form data includes a protected key
|
39
|
+
// - the form action includes a protected querystring key
|
40
|
+
//
|
41
|
+
const data = new FormData(form)
|
42
|
+
const params = createURLObject(form.action)?.searchParams || new URLSearchParams()
|
43
|
+
const inputs = list.map(item => item.input)
|
44
|
+
const checks = inputs.map(input => {
|
45
|
+
if (input.name) {
|
46
|
+
if (data.has(input.name)) return false
|
47
|
+
if (params.has(input.name)) return false
|
48
|
+
}
|
49
|
+
|
50
|
+
if (input.id) {
|
51
|
+
if (data.has(input.id)) return false
|
52
|
+
if (params.has(input.id)) return false
|
53
|
+
}
|
54
|
+
|
55
|
+
return true
|
56
|
+
})
|
57
|
+
if (checks.includes(false)) return false
|
58
|
+
return true
|
59
|
+
}
|
60
|
+
|
61
|
+
function submit(event) {
|
62
|
+
if (shouldSubmit(event.target)) return
|
63
|
+
event.preventDefault()
|
64
|
+
}
|
65
|
+
|
66
|
+
function monitor(form) {
|
67
|
+
form.removeEventListener('submit', submit, true)
|
68
|
+
form.addEventListener('submit', submit, true)
|
69
|
+
}
|
70
|
+
|
71
|
+
function patch() {
|
72
|
+
if (_patch) return
|
73
|
+
|
74
|
+
const orig = Document.prototype.createElement
|
75
|
+
|
76
|
+
_patch = {
|
77
|
+
value: function () {
|
78
|
+
const element = orig.apply(this, arguments)
|
79
|
+
|
80
|
+
try {
|
81
|
+
const tagName = String(arguments[0]).toUpperCase()
|
82
|
+
if (tagName === 'FORM') monitor(element)
|
83
|
+
} catch {}
|
84
|
+
|
85
|
+
return element
|
86
|
+
},
|
87
|
+
configurable: false
|
88
|
+
}
|
89
|
+
|
90
|
+
Object.defineProperty(Document.prototype, 'createElement', _patch)
|
91
|
+
}
|
92
|
+
|
93
|
+
function observe(attempt = 0) {
|
94
|
+
if (!document.body && attempt < 10) return setTimeout(() => observe(attempt + 1), 50)
|
95
|
+
|
96
|
+
if (_observer) return
|
97
|
+
|
98
|
+
_observer = new MutationObserver(mutations =>
|
99
|
+
mutations.forEach(mutation =>
|
100
|
+
mutation.addedNodes.forEach(node => {
|
101
|
+
if (node instanceof HTMLFormElement) monitor(node)
|
102
|
+
})
|
103
|
+
)
|
104
|
+
)
|
105
|
+
|
106
|
+
_observer.observe(document.body, {
|
107
|
+
childList: true,
|
108
|
+
subtree: true
|
109
|
+
})
|
110
|
+
}
|
111
|
+
|
112
|
+
addEventListener('load', () => observe())
|
113
|
+
patch()
|
114
|
+
observe()
|
115
|
+
|
116
|
+
// monitor all forms
|
117
|
+
document.querySelectorAll('form').forEach(form => monitor(form))
|
118
|
+
|
119
|
+
// TODO: protect XHR POST requests
|
120
|
+
// addEventListener('readystatechange', event => {
|
121
|
+
// const xhr = event.target
|
122
|
+
// if (xhr instanceof XMLHttpRequest && xhr.readyState === 1) {
|
123
|
+
// // if (should not submit)
|
124
|
+
// // xhr.send = xhr.abort
|
125
|
+
// }
|
126
|
+
// })
|
127
|
+
|
128
|
+
// TODO: protect fetch POST requests
|
129
|
+
// addEventListener('fetch', event => {
|
130
|
+
// // if (should not submit)
|
131
|
+
// // event.preventDefault()
|
132
|
+
// })
|
133
|
+
|
134
|
+
export { protect as protectForm }
|
data/app/javascript/guard.js
CHANGED
@@ -1,64 +1,46 @@
|
|
1
|
-
|
1
|
+
import { protectForm } from './forms'
|
2
2
|
|
3
3
|
export default class Guard {
|
4
4
|
constructor(controller) {
|
5
5
|
this.controller = controller
|
6
|
-
controller.element.addEventListener('trix-file-accept', event => event.preventDefault())
|
7
6
|
}
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
preventAttachments() {
|
9
|
+
this.editor?.removeAttribute('data-direct-upload-url')
|
10
|
+
this.editor?.removeAttribute('data-blob-url-template')
|
11
|
+
this.editor?.addEventListener('trix-file-accept', event => event.preventDefault(), true)
|
12
|
+
this.toolbar?.querySelector('[data-trix-button-group="file-tools"]')?.remove()
|
13
13
|
}
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
const
|
18
|
-
|
19
|
-
|
15
|
+
async preventLinks() {
|
16
|
+
const allowed = await this.controller.allowedLinkHosts
|
17
|
+
const blocked = await this.controller.blockedLinkHosts
|
18
|
+
if (!blocked.length && allowed.includes('*')) return
|
19
|
+
this.toolbar?.querySelector('[data-trix-action="link"]')?.remove()
|
20
|
+
}
|
20
21
|
|
21
|
-
|
22
|
-
|
23
|
-
document.addEventListener('submit', handlers[key], true)
|
22
|
+
protect(attempt = 0) {
|
23
|
+
if (!this.toolbar && attempt < 10) return setTimeout(() => this.protect(attempt + 1), 25)
|
24
24
|
|
25
|
-
|
26
|
-
|
27
|
-
const { addedNodes, target, type } = mutation
|
25
|
+
this.preventAttachments()
|
26
|
+
this.preventLinks()
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
if (target.closest('form')?.action === form.action)
|
32
|
-
if (target.id === input.id || target.name === input.name) target.remove()
|
33
|
-
break
|
34
|
-
case 'childList':
|
35
|
-
addedNodes.forEach(node => {
|
36
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
37
|
-
if (node.tagName.match(/^form$/i) && node.action === form.action) node.remove()
|
38
|
-
if (target.closest('form')?.action === form.action)
|
39
|
-
if (node.id === input.id || node.name === input.name) node.remove()
|
40
|
-
}
|
41
|
-
})
|
42
|
-
break
|
43
|
-
}
|
44
|
-
})
|
45
|
-
})
|
28
|
+
if (this.form) protectForm(this.form, this.input)
|
29
|
+
}
|
46
30
|
|
47
|
-
|
48
|
-
|
49
|
-
attributes: true,
|
50
|
-
childList: true,
|
51
|
-
subtree: true
|
52
|
-
})
|
31
|
+
get editor() {
|
32
|
+
return this.controller.element
|
53
33
|
}
|
54
34
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
35
|
+
get toolbar() {
|
36
|
+
return this.controller.toolbarElement
|
37
|
+
}
|
38
|
+
|
39
|
+
get form() {
|
40
|
+
return this.controller.formElement
|
41
|
+
}
|
59
42
|
|
60
|
-
|
61
|
-
|
62
|
-
trix?.remove()
|
43
|
+
get input() {
|
44
|
+
return this.controller.inputElement
|
63
45
|
}
|
64
46
|
}
|
data/app/javascript/index.js
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
import metadata from './metadata'
|
1
2
|
import { generateKey, encryptValues, generateKeyAndEncryptValues } from './encryption'
|
2
3
|
import { getTrixEmbedControllerClass } from './controller'
|
3
4
|
|
5
|
+
let initialized = false
|
6
|
+
|
4
7
|
const defaultOptions = {
|
5
8
|
application: null,
|
6
9
|
Controller: null,
|
@@ -8,15 +11,18 @@ const defaultOptions = {
|
|
8
11
|
}
|
9
12
|
|
10
13
|
function initialize(options = defaultOptions) {
|
14
|
+
if (initialized) return
|
11
15
|
const { application, Controller, Trix } = options
|
12
16
|
application.register('trix-embed', getTrixEmbedControllerClass({ Controller, Trix }))
|
17
|
+
initialized = true
|
13
18
|
}
|
14
19
|
|
15
20
|
self.TrixEmbed = {
|
16
|
-
|
17
|
-
generateKey,
|
21
|
+
...metadata,
|
18
22
|
encryptValues,
|
19
|
-
|
23
|
+
generateKey,
|
24
|
+
generateKeyAndEncryptValues,
|
25
|
+
initialize
|
20
26
|
}
|
21
27
|
|
22
28
|
export default self.TrixEmbed
|
data/app/javascript/media.js
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
import {
|
1
|
+
import { createURLObject } from './urls'
|
2
|
+
|
3
|
+
// Matches server side configuration
|
4
|
+
// SEE: lib/trix_embed/engine.rb
|
5
|
+
export const trixEmbedMediaTypes = {
|
6
|
+
attachment: 'trix-embed/attachment'
|
7
|
+
}
|
2
8
|
|
3
9
|
const audioMediaTypes = {
|
4
10
|
mp3: 'audio/mpeg', // MP3 audio format
|
@@ -74,6 +80,10 @@ const tagsWithSrcAttribute = [
|
|
74
80
|
'use' // SVG: Reuse shapes from other documents
|
75
81
|
]
|
76
82
|
|
83
|
+
// TODO: move to schema.js
|
84
|
+
export const trixEditorTagName = 'trix-editor'
|
85
|
+
export const trixAttachmentTagName = 'action-text-attachment'
|
86
|
+
|
77
87
|
export const mediaTags = tagsWithHrefAttribute.concat(tagsWithSrcAttribute)
|
78
88
|
|
79
89
|
export function isAudio(url) {
|
@@ -91,7 +101,7 @@ export function isVideo(url) {
|
|
91
101
|
export function getMediaType(value) {
|
92
102
|
let url
|
93
103
|
|
94
|
-
|
104
|
+
url = createURLObject(value)
|
95
105
|
if (!url) return null
|
96
106
|
|
97
107
|
const index = url.pathname.lastIndexOf('.')
|
data/app/javascript/renderer.js
CHANGED
@@ -1,6 +1,95 @@
|
|
1
|
-
import {
|
2
|
-
import { isImage, getMediaType } from './media'
|
3
|
-
import
|
1
|
+
import { createURLObject, extractURLHosts } from './urls'
|
2
|
+
import { isImage, getMediaType, trixAttachmentTagName, trixEmbedMediaTypes } from './media'
|
3
|
+
import templates from './templates'
|
4
|
+
|
5
|
+
// Matches server side configuration
|
6
|
+
// SEE: TrixEmbed::Attachment::ALLOWED_TAGS (app/models/trix_embed/attachment.rb)
|
7
|
+
const ALLOWED_TAGS = [
|
8
|
+
trixAttachmentTagName,
|
9
|
+
'a',
|
10
|
+
'abbr',
|
11
|
+
'acronym',
|
12
|
+
'address',
|
13
|
+
'b',
|
14
|
+
'big',
|
15
|
+
'blockquote',
|
16
|
+
'br',
|
17
|
+
'cite',
|
18
|
+
'code',
|
19
|
+
'dd',
|
20
|
+
'del',
|
21
|
+
'dfn',
|
22
|
+
'div',
|
23
|
+
'dl',
|
24
|
+
'dt',
|
25
|
+
'em',
|
26
|
+
'figcaption',
|
27
|
+
'figure',
|
28
|
+
'h1',
|
29
|
+
'h2',
|
30
|
+
'h3',
|
31
|
+
'h4',
|
32
|
+
'h5',
|
33
|
+
'h6',
|
34
|
+
'hr',
|
35
|
+
'i',
|
36
|
+
'iframe',
|
37
|
+
'img',
|
38
|
+
'ins',
|
39
|
+
'kbd',
|
40
|
+
'li',
|
41
|
+
'ol',
|
42
|
+
'p',
|
43
|
+
'pre',
|
44
|
+
'samp',
|
45
|
+
'small',
|
46
|
+
'span',
|
47
|
+
'strong',
|
48
|
+
'sub',
|
49
|
+
'sup',
|
50
|
+
'time',
|
51
|
+
'tt',
|
52
|
+
'ul',
|
53
|
+
'var'
|
54
|
+
]
|
55
|
+
|
56
|
+
// Matches server side configuration
|
57
|
+
// SEE: TrixEmbed::Attachment::ALLOWED_ATTRIBUTES (app/models/trix_embed/attachment.rb)
|
58
|
+
const ALLOWED_ATTRIBUTES = [
|
59
|
+
'abbr',
|
60
|
+
'allow',
|
61
|
+
'allowfullscreen',
|
62
|
+
'allowpaymentrequest',
|
63
|
+
'alt',
|
64
|
+
'caption',
|
65
|
+
'cite',
|
66
|
+
'content-type',
|
67
|
+
'credentialless',
|
68
|
+
'csp',
|
69
|
+
'data-trix-embed',
|
70
|
+
'data-trix-embed-error',
|
71
|
+
'data-trix-embed-prohibited',
|
72
|
+
'data-trix-embed-warning',
|
73
|
+
'datetime',
|
74
|
+
'filename',
|
75
|
+
'filesize',
|
76
|
+
'height',
|
77
|
+
'href',
|
78
|
+
'lang',
|
79
|
+
'loading',
|
80
|
+
'name',
|
81
|
+
'presentation',
|
82
|
+
'previewable',
|
83
|
+
'referrerpolicy',
|
84
|
+
'sandbox',
|
85
|
+
'sgid',
|
86
|
+
'src',
|
87
|
+
'srcdoc',
|
88
|
+
'title',
|
89
|
+
'url',
|
90
|
+
'width',
|
91
|
+
'xml:lang'
|
92
|
+
]
|
4
93
|
|
5
94
|
export default class Renderer {
|
6
95
|
// Constructs a new Renderer instance
|
@@ -11,50 +100,40 @@ export default class Renderer {
|
|
11
100
|
this.initializeTempates()
|
12
101
|
}
|
13
102
|
|
14
|
-
|
15
|
-
const
|
16
|
-
|
103
|
+
sanitize(html) {
|
104
|
+
const template = document.createElement('template')
|
105
|
+
template.innerHTML = `<div>${html}</div>`
|
106
|
+
const element = template.content.firstElementChild
|
107
|
+
const all = [element].concat([...element.querySelectorAll('*')])
|
108
|
+
all.forEach(el => {
|
109
|
+
if (ALLOWED_TAGS.includes(el.tagName.toLowerCase())) {
|
110
|
+
;[...el.attributes].forEach(attr => {
|
111
|
+
if (!ALLOWED_ATTRIBUTES.includes(attr.name.toLowerCase())) el.removeAttribute(attr.name)
|
112
|
+
})
|
113
|
+
} else {
|
114
|
+
el.remove()
|
115
|
+
}
|
116
|
+
})
|
117
|
+
return element.innerHTML
|
17
118
|
}
|
18
119
|
|
19
|
-
|
20
|
-
|
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)
|
120
|
+
initializeTempates() {
|
121
|
+
this.templates = templates
|
122
|
+
Object.keys(templates).forEach(name => this.initializeTemplate(name))
|
26
123
|
}
|
27
124
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
const h1 = header.tagName.match(/h1/i) ? header : header.querySelector('h1')
|
36
|
-
h1.innerHTML = html
|
37
|
-
return header.outerHTML
|
125
|
+
initializeTemplate(name) {
|
126
|
+
const property = `${name}TemplateValue`
|
127
|
+
const id = this.controller[property]
|
128
|
+
const template = id ? document.getElementById(id)?.innerHTML?.trim() : null
|
129
|
+
this.controller[property] = null
|
130
|
+
if (template) this.templates[name] = template
|
131
|
+
return this.templates[name]
|
38
132
|
}
|
39
133
|
|
40
|
-
|
41
|
-
|
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>`
|
134
|
+
render(templateName, params = {}) {
|
135
|
+
const template = this.templates[templateName]
|
136
|
+
return template.replace(/{{(.*?)}}/g, (_, key) => key.split('.').reduce((obj, k) => obj[k], params))
|
58
137
|
}
|
59
138
|
|
60
139
|
// TOOO: add support for audio and video
|
@@ -64,19 +143,8 @@ export default class Renderer {
|
|
64
143
|
// @returns {String} HTML
|
65
144
|
//
|
66
145
|
renderEmbed(url = 'https://example.com') {
|
67
|
-
|
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
|
146
|
+
const html = isImage(url) ? this.render('image', { src: url }) : this.render('iframe', { src: url })
|
147
|
+
return this.sanitize(html)
|
80
148
|
}
|
81
149
|
|
82
150
|
// Renders a list of URLs as HTML embeds i.e. iframes or media tags (img, video, audio etc.)
|
@@ -95,33 +163,45 @@ export default class Renderer {
|
|
95
163
|
// @param {String[]} allowedHosts - list of allowed hosts
|
96
164
|
// @returns {String} HTML
|
97
165
|
//
|
98
|
-
|
166
|
+
renderWarnings(urls = ['https://example.com', 'https://test.com'], allowedHosts = [], blockedHosts = []) {
|
99
167
|
if (!urls?.length) return
|
100
168
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
if (
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
169
|
+
allowedHosts = [...allowedHosts].sort()
|
170
|
+
if (allowedHosts.includes('*')) allowedHosts.splice(allowedHosts.indexOf('*'), 1)
|
171
|
+
|
172
|
+
blockedHosts = [...blockedHosts]
|
173
|
+
if (blockedHosts.includes('*')) blockedHosts.splice(blockedHosts.indexOf('*'), 1)
|
174
|
+
|
175
|
+
const hosts = [...new Set([...blockedHosts, ...extractURLHosts(urls)])].sort()
|
176
|
+
|
177
|
+
return this.render('warning', {
|
178
|
+
header: 'Copy/Paste Warning',
|
179
|
+
subheader: 'Content includes URLs or media from prohibited hosts or restricted protocols.',
|
180
|
+
prohibited: {
|
181
|
+
header: 'Prohibited Hosts',
|
182
|
+
hosts: hosts.length
|
183
|
+
? hosts.map(host => `<li>${host}</li>`).join('')
|
184
|
+
: '<li>URLs and media are restricted to allowed hosts and standard protocols.</li>'
|
185
|
+
},
|
186
|
+
allowed: {
|
187
|
+
header: 'Allowed Hosts',
|
188
|
+
hosts: allowedHosts.length
|
189
|
+
? allowedHosts.map(host => `<li>${host}</li>`).join('')
|
190
|
+
: '<li>Allowed hosts not configured.</li>'
|
191
|
+
}
|
192
|
+
})
|
114
193
|
}
|
115
194
|
|
116
|
-
// Renders
|
195
|
+
// Renders a JavaScript error
|
117
196
|
//
|
118
|
-
// @param {
|
197
|
+
// @param {Error} error - The error or exception
|
119
198
|
// @returns {String} HTML
|
120
199
|
//
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
200
|
+
renderError(error) {
|
201
|
+
return this.render('error', {
|
202
|
+
header: 'Unhandled Exception!',
|
203
|
+
subheader: 'Report this problem to a software engineer.',
|
204
|
+
error: error
|
205
|
+
})
|
126
206
|
}
|
127
207
|
}
|
data/app/javascript/store.js
CHANGED
@@ -1,7 +1,19 @@
|
|
1
|
+
import { createURLObject } from './urls'
|
2
|
+
|
1
3
|
export default class Store {
|
2
4
|
constructor(controller) {
|
5
|
+
const identifiers = [
|
6
|
+
location.pathname,
|
7
|
+
createURLObject(controller.formElement?.action)?.pathname,
|
8
|
+
controller.element.closest('[id]')?.id
|
9
|
+
]
|
10
|
+
|
3
11
|
this.controller = controller
|
4
|
-
this.
|
12
|
+
this.identifier = identifiers
|
13
|
+
.filter(i => i && i.length)
|
14
|
+
.join('/')
|
15
|
+
.replace(/\/{2,}/g, '/')
|
16
|
+
this.base = this.obfuscate(this.identifier)
|
5
17
|
}
|
6
18
|
|
7
19
|
split(list) {
|
data/app/javascript/templates.js
CHANGED
@@ -1,36 +1,53 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
image: `<img></img>`,
|
1
|
+
export default {
|
2
|
+
// inline templates ........................................................................................
|
3
|
+
link: `<a href='{{url}}'>{{label}}</a>`,
|
5
4
|
|
5
|
+
embedded: `
|
6
|
+
<span>
|
7
|
+
<strong>{{label}}</strong>
|
8
|
+
<span>{{description}}</span>
|
9
|
+
<del>{{url}}</del>
|
10
|
+
</span>
|
11
|
+
`,
|
12
|
+
|
13
|
+
prohibited: `
|
14
|
+
<span>
|
15
|
+
<strong>{{label}}</strong>
|
16
|
+
<span>{{description}}</span>
|
17
|
+
<del>{{url}}</del>
|
18
|
+
</span>
|
19
|
+
`,
|
20
|
+
|
21
|
+
// attachment templates ....................................................................................
|
6
22
|
error: `
|
7
|
-
<div>
|
8
|
-
<h1>
|
9
|
-
<
|
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>
|
23
|
+
<div data-trix-embed data-trix-embed-error>
|
24
|
+
<h1>{{header}}</h1>
|
25
|
+
<pre><code>{{error.stack}}</code></pre>
|
20
26
|
</div>
|
21
27
|
`,
|
22
28
|
|
23
|
-
|
24
|
-
<div
|
25
|
-
<
|
26
|
-
<p>Show a programmer the message below.</p>
|
27
|
-
<pre style="background-color:darkslategray; color:whitesmoke; padding:10px;"><code></code></pre>
|
29
|
+
iframe: `
|
30
|
+
<div data-trix-embed>
|
31
|
+
<iframe src='{{src}}' loading='lazy' referrerpolicy='no-referrer' scrolling='no'></iframe>
|
28
32
|
</div>
|
29
|
-
|
30
|
-
}
|
33
|
+
`,
|
31
34
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
35
|
+
image: `
|
36
|
+
<div data-trix-embed>
|
37
|
+
<img src='{{src}}' loading='lazy'></img>
|
38
|
+
</div>
|
39
|
+
`,
|
40
|
+
|
41
|
+
warning: `
|
42
|
+
<div data-trix-embed data-trix-embed-warning>
|
43
|
+
<h1>{{header}}</h1>
|
44
|
+
<h3>{{subheader}}</h3>
|
45
|
+
|
46
|
+
<h2>{{prohibited.header}}</h2>
|
47
|
+
<ul>{{prohibited.hosts}}</ul>
|
48
|
+
|
49
|
+
<h2>{{allowed.header}}</h2>
|
50
|
+
<ul>{{allowed.hosts}}</ul>
|
51
|
+
</div>
|
52
|
+
`
|
36
53
|
}
|