trix_embed 0.0.2 → 0.0.4
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 +4 -4
- data/README.md +120 -47
- data/app/assets/builds/trix-embed.js +5 -22
- data/app/assets/builds/trix-embed.metafile.json +1 -1
- data/app/javascript/controller.js +343 -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 +153 -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,43 @@ 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 = ['All']
|
171
|
+
|
172
|
+
blockedHosts = [...new Set([...blockedHosts, ...extractURLHosts(urls)])].sort()
|
173
|
+
if (blockedHosts.includes('*')) blockedHosts = ['All']
|
174
|
+
|
175
|
+
return this.render('warning', {
|
176
|
+
header: 'Copy/Paste Warning',
|
177
|
+
subheader: 'Content includes links or media from restricted protocols or prohibited hosts.',
|
178
|
+
prohibited: {
|
179
|
+
header: 'Prohibited Hosts',
|
180
|
+
hosts: blockedHosts.length
|
181
|
+
? blockedHosts.map(host => `<li>${host}</li>`).join('')
|
182
|
+
: '<li>Not configured</li>'
|
183
|
+
},
|
184
|
+
allowed: {
|
185
|
+
header: 'Allowed Hosts',
|
186
|
+
hosts: allowedHosts.length
|
187
|
+
? allowedHosts.map(host => `<li>${host}</li>`).join('')
|
188
|
+
: '<li>Not configured</li>'
|
189
|
+
}
|
190
|
+
})
|
114
191
|
}
|
115
192
|
|
116
|
-
// Renders
|
193
|
+
// Renders a JavaScript error
|
117
194
|
//
|
118
|
-
// @param {
|
195
|
+
// @param {Error} error - The error or exception
|
119
196
|
// @returns {String} HTML
|
120
197
|
//
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
198
|
+
renderError(error) {
|
199
|
+
return this.render('error', {
|
200
|
+
header: 'Unhandled Exception!',
|
201
|
+
subheader: 'Report this problem to a software engineer.',
|
202
|
+
error: error
|
203
|
+
})
|
126
204
|
}
|
127
205
|
}
|
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
|
}
|