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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 38ead3fd6cca0b10aca5f45b5602da75b8674b3c71a22ac5d9140c23ce7ceabe
|
4
|
+
data.tar.gz: 95691446e65997da2a828c6bf48e714c958fc0819f81e039ea7301f717f9b342
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2d33ed333f7125d7cd932781b28b7c740859ea271d3dc00e2b5006ac12f9818408873518aa5d3c17b9be431f3c29b3103c32fb71275cf67b34af6961c2419236
|
7
|
+
data.tar.gz: a86192046fc34ba9b3b69f8851de2723307a3c9d2e43856ba20eb8a4c06a5efed24f07eaeb493eb6abf8a83dcc0d63c913635f4bae1ee9d7581e48552a8b301d
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2023 Nate Hopkins (hopsoft)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Trix Embed
|
2
|
+
|
3
|
+
A Stimulus controller to safely embed external media in the Trix editor.
|
4
|
+
|
5
|
+
## Setup
|
6
|
+
|
7
|
+
```sh
|
8
|
+
yarn add @hotwired/stimulus trix trix-embed
|
9
|
+
```
|
10
|
+
|
11
|
+
```js
|
12
|
+
import 'trix'
|
13
|
+
import { Application, Controller } from '@hotwired/stimulus'
|
14
|
+
import TrixEmbed from 'trix-embed'
|
15
|
+
|
16
|
+
const application = Application.start()
|
17
|
+
TrixEmbed.initialize({ application, Controller, Trix })
|
18
|
+
```
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
```html
|
23
|
+
<form>
|
24
|
+
<input id="content" name="content" type="hidden">
|
25
|
+
<trix-editor id="editor" input="content"
|
26
|
+
data-controller="trix-embed"
|
27
|
+
data-action="trix-paste->trix-embed#paste"
|
28
|
+
data-trix-embed-hosts-value='["example.com", "test.com"]'>
|
29
|
+
</trix-editor>
|
30
|
+
</form>
|
31
|
+
```
|
32
|
+
|
33
|
+
## Sponsors
|
34
|
+
|
35
|
+
<p align="center">
|
36
|
+
<em>Proudly sponsored by</em>
|
37
|
+
</p>
|
38
|
+
<p align="center">
|
39
|
+
<a href="https://www.clickfunnels.com?utm_source=hopsoft&utm_medium=open-source&utm_campaign=trix_embed">
|
40
|
+
<img src="https://images.clickfunnel.com/uploads/digital_asset/file/176632/clickfunnels-dark-logo.svg" width="575" />
|
41
|
+
</a>
|
42
|
+
</p>
|
43
|
+
|
44
|
+
## Developing
|
45
|
+
|
46
|
+
```sh
|
47
|
+
git clone https://github.com/hopsoft/trix_embed.git
|
48
|
+
cd trix_embed
|
49
|
+
yarn
|
50
|
+
yarn build
|
51
|
+
yarn dev
|
52
|
+
```
|
53
|
+
### Docker
|
54
|
+
|
55
|
+
This project supports a fully Dockerized development experience.
|
56
|
+
|
57
|
+
1. Simply run the following commands to get started.
|
58
|
+
|
59
|
+
```sh
|
60
|
+
git clone -o github https://github.com/hopsoft/trix_embed.git
|
61
|
+
cd trix_embed
|
62
|
+
```
|
63
|
+
|
64
|
+
```sh
|
65
|
+
docker compose up -d # start the envionment (will take a few minutes on 1st run)
|
66
|
+
open http://localhost:3000 # in a browser
|
67
|
+
```
|
68
|
+
|
69
|
+
And, if you're using the [containers gem (WIP)](https://github.com/hopsoft/containers).
|
70
|
+
|
71
|
+
```sh
|
72
|
+
containers up # start the envionment (will take a few minutes on 1st run)
|
73
|
+
open http://localhost:3000 # in a browser
|
74
|
+
```
|
75
|
+
|
76
|
+
1. Edit files using your preferred tools on the host machine.
|
77
|
+
|
78
|
+
1. That's it!
|
79
|
+
|
80
|
+
## Releasing
|
81
|
+
|
82
|
+
1. Run `yarn` and `bundle` to pick up the latest
|
83
|
+
1. Bump version number at `lib/trix_embed/version.rb`. Pre-release versions use `.preN`
|
84
|
+
1. Run `yarn build`
|
85
|
+
1. Commit and push changes to GitHub
|
86
|
+
1. Run `rake release`
|
87
|
+
1. Run `yarn publish --no-git-tag-version --access public`
|
88
|
+
1. Yarn will prompt you for the new version. Pre-release versions use `-preN`
|
89
|
+
1. Commit and push changes to GitHub
|
90
|
+
1. Create a new release on GitHub ([here](https://github.com/hopsoft/trix_embed/releases)) and generate the changelog for the stable release for it
|
91
|
+
|
92
|
+
## License
|
93
|
+
|
94
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,22 @@
|
|
1
|
+
var G=Object.defineProperty,X=Object.defineProperties;var Q=Object.getOwnPropertyDescriptors;var P=Object.getOwnPropertySymbols;var Y=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var M=(n,e,t)=>e in n?G(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t,U=(n,e)=>{for(var t in e||(e={}))Y.call(e,t)&&M(n,t,e[t]);if(P)for(var t of P(e))Z.call(e,t)&&M(n,t,e[t]);return n},N=(n,e)=>X(n,Q(e));var S=(n,e,t)=>(M(n,typeof e!="symbol"?e+"":e,t),t);var A={name:"AES-GCM",length:256},ee=!0,te=["encrypt","decrypt"];async function re(){let e=["encrypt","decrypt"];return await crypto.subtle.generateKey(A,!0,e)}async function ie(n){let e=await crypto.subtle.exportKey("jwk",n);return JSON.stringify(e)}async function O(n){let e=JSON.parse(n);return await crypto.subtle.importKey("jwk",e,A,ee,te)}async function ne(n,e){let t=new TextEncoder().encode(String(n)),i=crypto.getRandomValues(new Uint8Array(12)),r=await crypto.subtle.encrypt(N(U({},A),{iv:i}),e,t),o={ciphertext:btoa(String.fromCharCode(...new Uint8Array(r))),iv:btoa(String.fromCharCode(...i))};return btoa(JSON.stringify(o))}async function oe(n,e){let t=JSON.parse(atob(n)),i=new Uint8Array(atob(t.ciphertext).split("").map(s=>s.charCodeAt(0))),r=new Uint8Array(atob(t.iv).split("").map(s=>s.charCodeAt(0))),o=await crypto.subtle.decrypt(N(U({},A),{iv:r}),e,i);return new TextDecoder().decode(o)}async function v(){let n=await re(),e=await ie(n);return btoa(e)}async function b(n,e=[]){let t=await O(atob(n));return Promise.all(e.map(i=>ne(i,t)))}async function K(n,e=[]){let t=await O(atob(n));return Promise.all(e.map(i=>oe(i,t)))}async function $(n=[]){let e=await v(),t=await b(e,n);return console.log(`data-trix-embed-key-value="${e}"`),console.log(`data-trix-embed-hosts-value='${JSON.stringify(t)}'`),{key:e,encryptedValues:t}}function h(n,e=t=>{}){try{let t=new URL(String(n).trim());return e&&e(t),t}catch(t){console.info(`Failed to parse URL! value='${n}']`)}return null}function V(n,e=t=>{}){let t=null;return h(n,i=>t=i.host),t&&e&&e(t),t}function se(n){let e=[],t=document.createTreeWalker(n,NodeFilter.SHOW_TEXT,r=>r.nodeValue.includes("http")?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_REJECT),i;for(;i=t.nextNode();)i.nodeValue.split(/\s+/).filter(r=>r.startsWith("http")).forEach(r=>h(r,o=>{e.includes(o.href)||e.push(o.href)}));return e}function ae(n){let e=[];return n.src&&h(n.src,i=>e.push(i.href)),n.href&&h(n.href,i=>{e.includes(i.href)||e.push(i.href)}),n.querySelectorAll("[src], [href]").forEach(i=>{h(i.src||i.href,r=>{e.includes(r.href)||e.push(r.href)})}),e}function R(n,e=[]){let t=!1;return V(n,i=>t=!!e.find(r=>i.includes(r))),t}function I(n){return n.reduce((e,t)=>(V(t,i=>{e.includes(i)||e.push(i)}),e),[])}function q(n){let e=ae(n),t=se(n);return[...new Set([...e,...t])]}var D={avif:"image/avif",bmp:"image/bmp",gif:"image/gif",heic:"image/heic",heif:"image/heif",ico:"image/x-icon",jp2:"image/jp2",jpeg:"image/jpeg",jpg:"image/jpeg",jxr:"image/vnd.ms-photo",png:"image/png",svg:"image/svg+xml",tif:"image/tiff",tiff:"image/tiff",webp:"image/webp"};var ce=D,le=["animate","animateMotion","animateTransform","area","audio","base","embed","feDisplacementMap","feImage","feTile","filter","font-face-uri","iframe","image","link","object","script","source","track","use","video"],de=["audio","embed","iframe","img","input","script","source","track","video","frame","frameset","object","picture","use"],F=le.concat(de);function J(n){return!!Object.values(D).find(e=>e===H(n))}function H(n){let e;if(h(n,r=>e=r),!e)return null;let t=e.pathname.lastIndexOf(".");if(!t)return null;let i=e.pathname.substring(t+1);return ce[i]}var T=class{constructor(e){S(this,"protectSubmit",e=>{let t=this.controller.formElement,i=e.target.closest("form");i&&i.action===t.action&&i.method===t.method&&i!==t&&e.preventDefault()});this.controller=e,e.element.addEventListener("trix-file-accept",t=>t.preventDefault())}protect(){if(!this.controller.formElement)return;let e=this.controller.formElement,t=this.controller.inputElement,i=`${e.method}${e.action}`;document.removeEventListener("submit",handlers[i],!0),handlers[i]=this.protectSubmit.bind(this),document.addEventListener("submit",handlers[i],!0),new MutationObserver((o,s)=>{o.forEach(a=>{var x;let{addedNodes:u,target:d,type:p}=a;switch(p){case"attributes":((x=d.closest("form"))==null?void 0:x.action)===e.action&&(d.id===t.id||d.name===t.name)&&d.remove();break;case"childList":u.forEach(l=>{var y;l.nodeType===Node.ELEMENT_NODE&&(l.tagName.match(/^form$/i)&&l.action===e.action&&l.remove(),((y=d.closest("form"))==null?void 0:y.action)===e.action&&(l.id===t.id||l.name===t.name)&&l.remove())});break}})}).observe(document.body,{attributeFilter:["id","name"],attributes:!0,childList:!0,subtree:!0})}cleanup(){let e=this.controller.element,t=this.controller.inputElement,i=this.controller.toolbarElement;t==null||t.remove(),i==null||i.remove(),e==null||e.remove()}};var E=class{constructor(e){var t;this.controller=e,this.base=this.obfuscate([location.pathname,(t=this.controller.element.closest("[id]"))==null?void 0:t.id].join("/"))}split(e){let t=Math.ceil(e.length/2);return[e.slice(0,t),e.slice(t)]}obfuscate(e){var r;let t=[...e].map(o=>o.charCodeAt(0));return[(r=this.split(t)[1])==null?void 0:r.reverse(),t[0]].flat().join("")}read(e){return sessionStorage.getItem(this.generateStorageKey(e))}write(e,t){return sessionStorage.setItem(this.generateStorageKey(e),t)}remove(e){return sessionStorage.removeItem(this.generateStorageKey(e))}generateStorageKey(e){let t=[...this.obfuscate(e)],[i,r]=this.split(t);return btoa(`${i}/${this.base}/${r}`)}};var me={header:"<h1></h1>",iframe:"<iframe></iframe>",image:"<img></img>",error:`
|
2
|
+
<div>
|
3
|
+
<h1>Copy/Paste Info</h1>
|
4
|
+
<h3>The pasted content includes media from unsupported hosts.</h3>
|
5
|
+
|
6
|
+
<h2>Prohibited Hosts / Domains</h2>
|
7
|
+
<ul data-list="prohibited-hosts">
|
8
|
+
<li>Media is only supported from allowed hosts.</li>
|
9
|
+
</ul>
|
10
|
+
|
11
|
+
<h2>Allowed Hosts / Domains</h2>
|
12
|
+
<ul data-list="allowed-hosts">
|
13
|
+
<li>Allowed hosts not configured.</li>
|
14
|
+
</ul>
|
15
|
+
</div>
|
16
|
+
`,exception:`
|
17
|
+
<div style='background-color:lightyellow; color:red; border:solid 1px red; padding:20px;'>
|
18
|
+
<h1>Unhandled Exception!</h1>
|
19
|
+
<p>Show a programmer the message below.</p>
|
20
|
+
<pre style="background-color:darkslategray; color:whitesmoke; padding:10px;"><code></code></pre>
|
21
|
+
</div>
|
22
|
+
`};function z(n){let e=document.createElement("template");return e.innerHTML=me[n],e}var w=class{constructor(e){this.controller=e,this.initializeTempates()}initializeTempates(){["error","exception","header","iframe","image"].forEach(t=>this.initializeTemplate(t))}initializeTemplate(e){let t;this.controller[`has${e.charAt(0).toUpperCase()+e.slice(1)}TemplateValue`]&&(t=document.getElementById(this.controller[`${e}TemplateValue`])),this[`${e}Template`]=t||z(e)}renderHeader(e){let t=this.headerTemplate.content.firstElementChild.cloneNode(!0),i=t.tagName.match(/h1/i)?t:t.querySelector("h1");return i.innerHTML=e,t.outerHTML}renderLinks(e=["https://example.com","https://test.com"]){return e=e.filter(i=>{let r=!1;return h(i,o=>r=!0),r}).sort(),e.length?`<ul>${e.map(i=>`<li><a href='${i}'>${i}</a></li>`).join("")}</ul><br>`:void 0}renderEmbed(e="https://example.com"){let t;if(J(e)){t=this.imageTemplate.content.firstElementChild.cloneNode(!0);let i=t.tagName.match(/img/i)?t:t.querySelector("img");i.src=e}else{t=this.iframeTemplate.content.firstElementChild.cloneNode(!0);let i=t.tagName.match(/iframe/i)?t:t.querySelector("iframe");i.src=e}return t.outerHTML}renderEmbeds(e=["https://example.com","https://test.com"]){if(e!=null&&e.length)return e.map(t=>this.renderEmbed(t))}renderErrors(e=["https://example.com","https://test.com"],t=[]){if(!(e!=null&&e.length))return;let i=this.errorTemplate.content.firstElementChild.cloneNode(!0),r=i.querySelector('[data-list="prohibited-hosts"]'),o=i.querySelector('[data-list="allowed-hosts"]');if(r){let s=I(e).sort();s.length&&(r.innerHTML=s.map(a=>`<li>${a}</li>`).join(""))}return o&&t.length&&(o.innerHTML=t.map(s=>`<li>${s}</li>`).join("")),i.outerHTML}renderException(e){let t=this.exceptionTemplate.content.firstElementChild.cloneNode(!0),i=t.querySelector("code");return i.innerHTML=e.message,t.outerHTML}};var ue={Controller:null,Trix:null};function B(n=ue){var i;let{Controller:e,Trix:t}=n;return i=class extends e{async connect(){var r;this.store=new E(this),this.guard=new T(this),await this.rememberConfig(),this.paranoid&&this.guard.protect(),(r=this.toolbarElement.querySelector('[data-trix-button-group="file-tools"]'))==null||r.remove(),window.addEventListener("beforeunload",()=>this.disconnect())}disconnect(){this.paranoid&&this.guard.cleanup(),this.forgetConfig()}async paste(r){let{html:o,string:s,range:a}=r.paste,u=o||s||"",d=this.buildPastedTemplate(u),p=d.content.firstElementChild,l=this.sanitizePastedElement(p).innerHTML.trim(),y=q(p);if(!y.length)return;r.preventDefault(),this.editor.setSelectedRange(a);let C=await this.hosts,f=new w(this);try{let g=y.filter(c=>H(c));Array.from(d.content.firstElementChild.querySelectorAll("iframe")).forEach(c=>{g.includes(c.src)||g.push(c.src)});let k=g.filter(c=>R(c,C)),W=g.filter(c=>!k.includes(c)),j=y.filter(c=>!g.includes(c)),L=j.filter(c=>R(c,C)),_=j.filter(c=>!L.includes(c)),m;if(m=W,m.length&&await this.insert(f.renderErrors(m,C.sort())),m=_,m.length&&(await this.insert(f.renderHeader("Pasted URLs")),await this.insert(f.renderLinks(m),{disposition:"inline"})),m=k,m.length&&(m.length>1&&await this.insert(f.renderHeader("Embedded Media")),await this.insert(f.renderEmbeds(m))),m=L,m.length&&await this.insert(f.renderEmbeds(L)),k[0]===l||L[0]===l)return this.editor.insertLineBreak();l.length&&(await this.insert(f.renderHeader("Pasted Content",l)),this.editor.insertLineBreak(),this.insert(l,{disposition:"inline"}))}catch(g){this.insert(f.renderException(g))}}buildPastedTemplate(r){let o=document.createElement("template");return o.innerHTML=`<div>${r.trim()}</div>`,o}sanitizePastedElement(r){r=r.cloneNode(!0),r.querySelectorAll(F.join(", ")).forEach(a=>a.remove());let o=r.querySelectorAll("*"),s=r.innerHTML.match(/\r\n|\n|\r/g)||[];return(s.length?o.length/s.length:0)<=.1&&(r.innerHTML=r.innerHTML.replaceAll(/\r\n|\n|\r/g,"<br>")),r}insertAttachment(r,o={delay:0}){let{delay:s}=o;return new Promise(a=>{setTimeout(()=>{let u=new t.Attachment({content:r,contentType:"application/vnd.trix-embed"});this.editor.insertAttachment(u),a()},s)})}insertHTML(r,o={delay:0}){let{delay:s}=o;return new Promise(a=>{setTimeout(()=>{this.editor.insertHTML(r),this.editor.moveCursorInDirection("forward"),this.editor.insertLineBreak(),this.editor.moveCursorInDirection("backward"),a()},s)})}insert(r,o={delay:0,disposition:"attachment"}){let{delay:s,disposition:a}=o;return r!=null&&r.length?new Promise(u=>{setTimeout(()=>{if(typeof r=="string")return a==="inline"?this.insertHTML(r,{delay:s}).then(u):this.insertAttachment(r,{delay:s}).then(u);if(Array.isArray(r))return a==="inline"?r.reduce((d,p,x)=>d.then(this.insertHTML(p,{delay:s})),Promise.resolve()).then(u):r.reduce((d,p,x)=>d.then(this.insertAttachment(p,{delay:s})),Promise.resolve()).then(u);u()})}):Promise.resolve()}get editor(){return this.element.editor}get toolbarElement(){let r=this.element.previousElementSibling;return r!=null&&r.tagName.match(/trix-toolbar/i)?r:null}get inputElement(){return document.getElementById(this.element.getAttribute("input"))}get formElement(){return this.element.closest("form")}get paranoid(){return!!this.store.read("paranoid")}get key(){try{return JSON.parse(this.store.read("key"))[2]}catch(r){}}get hosts(){try{return K(this.key,JSON.parse(this.store.read("hosts")))}catch(r){return[]}}get reservedDomains(){return["example.com","test.com","invalid.com","example.cat","nic.example","example.co.uk"]}async rememberConfig(){let r=await v(),o=await b(r,this.reservedDomains),s=await b(r,this.hostsValue);this.store.write("key",JSON.stringify([o[0],o[1],r,o[2]])),this.element.removeAttribute("data-trix-embed-key-value"),this.store.write("hosts",JSON.stringify(s)),this.element.removeAttribute("data-trix-embed-hosts-value"),this.paranoidValue!==!1&&(this.store.write("paranoid",JSON.stringify(o.slice(3))),this.element.removeAttribute("data-trix-embed-paranoid"))}forgetConfig(){this.store.remove("key"),this.store.remove("hosts"),this.store.remove("paranoid")}},S(i,"values",{validTemplate:String,errorTemplate:String,headerTemplate:String,iframeTemplate:String,imageTemplate:String,hosts:Array,paranoid:{type:Boolean,default:!0}}),i}var he={application:null,Controller:null,Trix:null};function pe(n=he){let{application:e,Controller:t,Trix:i}=n;e.register("trix-embed",B({Controller:t,Trix:i}))}self.TrixEmbed={initialize:pe,generateKey:v,encryptValues:b,generateKeyAndEncryptValues:$};var qe=self.TrixEmbed;export{qe as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
{"inputs":{"app/javascript/encryption.js":{"bytes":4095,"imports":[{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/urls.js":{"bytes":2818,"imports":[],"format":"esm"},"app/javascript/media.js":{"bytes":3331,"imports":[{"path":"app/javascript/urls.js","kind":"import-statement","original":"./urls"}],"format":"esm"},"app/javascript/guard.js":{"bytes":2028,"imports":[{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/store.js":{"bytes":952,"imports":[],"format":"esm"},"app/javascript/templates.js":{"bytes":997,"imports":[],"format":"esm"},"app/javascript/renderer.js":{"bytes":4072,"imports":[{"path":"app/javascript/urls.js","kind":"import-statement","original":"./urls"},{"path":"app/javascript/media.js","kind":"import-statement","original":"./media"},{"path":"app/javascript/templates.js","kind":"import-statement","original":"./templates"}],"format":"esm"},"app/javascript/controller.js":{"bytes":9317,"imports":[{"path":"app/javascript/encryption.js","kind":"import-statement","original":"./encryption"},{"path":"app/javascript/urls.js","kind":"import-statement","original":"./urls"},{"path":"app/javascript/media.js","kind":"import-statement","original":"./media"},{"path":"app/javascript/guard.js","kind":"import-statement","original":"./guard"},{"path":"app/javascript/store.js","kind":"import-statement","original":"./store"},{"path":"app/javascript/renderer.js","kind":"import-statement","original":"./renderer"},{"path":"<runtime>","kind":"import-statement","external":true}],"format":"esm"},"app/javascript/index.js":{"bytes":548,"imports":[{"path":"app/javascript/encryption.js","kind":"import-statement","original":"./encryption"},{"path":"app/javascript/controller.js","kind":"import-statement","original":"./controller"}],"format":"esm"}},"outputs":{"app/assets/builds/trix-embed.js":{"imports":[],"exports":["default"],"entryPoint":"app/javascript/index.js","inputs":{"app/javascript/encryption.js":{"bytesInOutput":1346},"app/javascript/urls.js":{"bytesInOutput":973},"app/javascript/media.js":{"bytesInOutput":846},"app/javascript/guard.js":{"bytesInOutput":1285},"app/javascript/store.js":{"bytesInOutput":688},"app/javascript/templates.js":{"bytesInOutput":897},"app/javascript/renderer.js":{"bytesInOutput":1780},"app/javascript/controller.js":{"bytesInOutput":4300},"app/javascript/index.js":{"bytesInOutput":274}},"bytes":12893}}}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TrixEmbed
|
4
|
+
module ApplicationHelper
|
5
|
+
def trix_embed_attachment(local_assigns = {})
|
6
|
+
return local_assigns[:attachable] if local_assigns[:attachable].is_a?(TrixEmbed::Attachment)
|
7
|
+
return local_assigns[:attachment].attachable if local_assigns[:attachment]&.attachable.is_a?(TrixEmbed::Attachment)
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
import { generateKey, encryptValues, decryptValues } from './encryption'
|
2
|
+
import { extractURLsFromElement, validateURL } from './urls'
|
3
|
+
import { getMediaType, mediaTags } from './media'
|
4
|
+
import Guard from './guard'
|
5
|
+
import Store from './store'
|
6
|
+
import Renderer from './renderer'
|
7
|
+
|
8
|
+
const defaultOptions = {
|
9
|
+
Controller: null,
|
10
|
+
Trix: null
|
11
|
+
}
|
12
|
+
|
13
|
+
export function getTrixEmbedControllerClass(options = defaultOptions) {
|
14
|
+
const { Controller, Trix } = options
|
15
|
+
return class extends Controller {
|
16
|
+
static values = {
|
17
|
+
// templates
|
18
|
+
validTemplate: String, // dom id of template to use for valid embeds
|
19
|
+
errorTemplate: String, // dom id of template to use for invalid embeds
|
20
|
+
headerTemplate: String, // dom id of template to use for embed headers
|
21
|
+
iframeTemplate: String, // dom id of template to use for iframe embeds
|
22
|
+
imageTemplate: String, // dom id of template to use for image embeds
|
23
|
+
|
24
|
+
// security related values
|
25
|
+
hosts: Array, // list of hosts/domains that embeds are allowed from
|
26
|
+
paranoid: { type: Boolean, default: true } // guard against attacks
|
27
|
+
}
|
28
|
+
|
29
|
+
async connect() {
|
30
|
+
this.store = new Store(this)
|
31
|
+
this.guard = new Guard(this)
|
32
|
+
await this.rememberConfig()
|
33
|
+
if (this.paranoid) this.guard.protect()
|
34
|
+
this.toolbarElement.querySelector('[data-trix-button-group="file-tools"]')?.remove()
|
35
|
+
window.addEventListener('beforeunload', () => this.disconnect()) // TODO: this may not be necessary
|
36
|
+
}
|
37
|
+
|
38
|
+
disconnect() {
|
39
|
+
if (this.paranoid) this.guard.cleanup()
|
40
|
+
this.forgetConfig()
|
41
|
+
}
|
42
|
+
|
43
|
+
async paste(event) {
|
44
|
+
const { html, string, range } = event.paste
|
45
|
+
let content = html || string || ''
|
46
|
+
const pastedTemplate = this.buildPastedTemplate(content)
|
47
|
+
const pastedElement = pastedTemplate.content.firstElementChild
|
48
|
+
const sanitizedPastedElement = this.sanitizePastedElement(pastedElement)
|
49
|
+
const sanitizedPastedContent = sanitizedPastedElement.innerHTML.trim()
|
50
|
+
const pastedURLs = extractURLsFromElement(pastedElement)
|
51
|
+
|
52
|
+
// no URLs were pasted, let Trix handle it ...............................................................
|
53
|
+
if (!pastedURLs.length) return
|
54
|
+
|
55
|
+
event.preventDefault()
|
56
|
+
this.editor.setSelectedRange(range)
|
57
|
+
const hosts = await this.hosts
|
58
|
+
const renderer = new Renderer(this)
|
59
|
+
|
60
|
+
try {
|
61
|
+
// Media URLs (images, videos, audio etc.)
|
62
|
+
const mediaURLs = pastedURLs.filter(url => getMediaType(url))
|
63
|
+
Array.from(pastedTemplate.content.firstElementChild.querySelectorAll('iframe')).forEach(frame => {
|
64
|
+
if (!mediaURLs.includes(frame.src)) mediaURLs.push(frame.src)
|
65
|
+
})
|
66
|
+
const validMediaURLs = mediaURLs.filter(url => validateURL(url, hosts))
|
67
|
+
const invalidMediaURLs = mediaURLs.filter(url => !validMediaURLs.includes(url))
|
68
|
+
|
69
|
+
// Standard URLs (non-media resources i.e. web pages etc.)
|
70
|
+
const standardURLs = pastedURLs.filter(url => !mediaURLs.includes(url))
|
71
|
+
const validStandardURLs = standardURLs.filter(url => validateURL(url, hosts))
|
72
|
+
const invalidStandardURLs = standardURLs.filter(url => !validStandardURLs.includes(url))
|
73
|
+
|
74
|
+
let urls
|
75
|
+
|
76
|
+
// 1. render invalid media urls ..........................................................................
|
77
|
+
urls = invalidMediaURLs
|
78
|
+
if (urls.length) await this.insert(renderer.renderErrors(urls, hosts.sort()))
|
79
|
+
|
80
|
+
// 2. render invalid standard urls .......................................................................
|
81
|
+
urls = invalidStandardURLs
|
82
|
+
if (urls.length) {
|
83
|
+
await this.insert(renderer.renderHeader('Pasted URLs'))
|
84
|
+
await this.insert(renderer.renderLinks(urls), { disposition: 'inline' })
|
85
|
+
}
|
86
|
+
|
87
|
+
// 3. render valid media urls ............................................................................
|
88
|
+
urls = validMediaURLs
|
89
|
+
if (urls.length) {
|
90
|
+
if (urls.length > 1) await this.insert(renderer.renderHeader('Embedded Media'))
|
91
|
+
await this.insert(renderer.renderEmbeds(urls))
|
92
|
+
}
|
93
|
+
|
94
|
+
// 4. render valid standard urls .........................................................................
|
95
|
+
urls = validStandardURLs
|
96
|
+
if (urls.length) await this.insert(renderer.renderEmbeds(validStandardURLs))
|
97
|
+
|
98
|
+
// exit early if there is only one valid URL and it is the same as the pasted content
|
99
|
+
if (validMediaURLs[0] === sanitizedPastedContent || validStandardURLs[0] === sanitizedPastedContent)
|
100
|
+
return this.editor.insertLineBreak()
|
101
|
+
|
102
|
+
// 5. render the pasted content as sanitized HTML ........................................................
|
103
|
+
if (sanitizedPastedContent.length) {
|
104
|
+
await this.insert(renderer.renderHeader('Pasted Content', sanitizedPastedContent))
|
105
|
+
this.editor.insertLineBreak()
|
106
|
+
this.insert(sanitizedPastedContent, { disposition: 'inline' })
|
107
|
+
}
|
108
|
+
} catch (ex) {
|
109
|
+
this.insert(renderer.renderException(ex))
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
buildPastedTemplate(content) {
|
114
|
+
const template = document.createElement('template')
|
115
|
+
template.innerHTML = `<div>${content.trim()}</div>`
|
116
|
+
return template
|
117
|
+
}
|
118
|
+
|
119
|
+
sanitizePastedElement(element) {
|
120
|
+
element = element.cloneNode(true)
|
121
|
+
element.querySelectorAll(mediaTags.join(', ')).forEach(tag => tag.remove())
|
122
|
+
|
123
|
+
const tags = element.querySelectorAll('*')
|
124
|
+
const newlines = element.innerHTML.match(/\r\n|\n|\r/g) || []
|
125
|
+
|
126
|
+
// replace newlines with <br> if there are <= 10% tags to newlines
|
127
|
+
if ((newlines.length ? tags.length / newlines.length : 0) <= 0.1)
|
128
|
+
element.innerHTML = element.innerHTML.replaceAll(/\r\n|\n|\r/g, '<br>')
|
129
|
+
|
130
|
+
return element
|
131
|
+
}
|
132
|
+
|
133
|
+
insertAttachment(content, options = { delay: 0 }) {
|
134
|
+
const { delay } = options
|
135
|
+
return new Promise(resolve => {
|
136
|
+
setTimeout(() => {
|
137
|
+
const attachment = new Trix.Attachment({ content, contentType: 'application/vnd.trix-embed' })
|
138
|
+
this.editor.insertAttachment(attachment)
|
139
|
+
resolve()
|
140
|
+
}, delay)
|
141
|
+
})
|
142
|
+
}
|
143
|
+
|
144
|
+
insertHTML(content, options = { delay: 0 }) {
|
145
|
+
const { delay } = options
|
146
|
+
return new Promise(resolve => {
|
147
|
+
setTimeout(() => {
|
148
|
+
this.editor.insertHTML(content)
|
149
|
+
// shenanigans to ensure that Trix considers this block of content closed
|
150
|
+
this.editor.moveCursorInDirection('forward')
|
151
|
+
this.editor.insertLineBreak()
|
152
|
+
this.editor.moveCursorInDirection('backward')
|
153
|
+
resolve()
|
154
|
+
}, delay)
|
155
|
+
})
|
156
|
+
}
|
157
|
+
|
158
|
+
insert(content, options = { delay: 0, disposition: 'attachment' }) {
|
159
|
+
const { delay, disposition } = options
|
160
|
+
|
161
|
+
if (content?.length) {
|
162
|
+
return new Promise(resolve => {
|
163
|
+
setTimeout(() => {
|
164
|
+
if (typeof content === 'string') {
|
165
|
+
if (disposition === 'inline') return this.insertHTML(content, { delay }).then(resolve)
|
166
|
+
else return this.insertAttachment(content, { delay }).then(resolve)
|
167
|
+
}
|
168
|
+
|
169
|
+
if (Array.isArray(content)) {
|
170
|
+
if (disposition === 'inline')
|
171
|
+
return content
|
172
|
+
.reduce((p, c, i) => p.then(this.insertHTML(c, { delay })), Promise.resolve())
|
173
|
+
.then(resolve)
|
174
|
+
else
|
175
|
+
return content
|
176
|
+
.reduce((p, c, i) => p.then(this.insertAttachment(c, { delay })), Promise.resolve())
|
177
|
+
.then(resolve)
|
178
|
+
}
|
179
|
+
|
180
|
+
resolve()
|
181
|
+
})
|
182
|
+
})
|
183
|
+
}
|
184
|
+
|
185
|
+
return Promise.resolve()
|
186
|
+
}
|
187
|
+
|
188
|
+
// Returns the Trix editor
|
189
|
+
//
|
190
|
+
// @returns {TrixEditor}
|
191
|
+
//
|
192
|
+
get editor() {
|
193
|
+
return this.element.editor
|
194
|
+
}
|
195
|
+
|
196
|
+
get toolbarElement() {
|
197
|
+
const sibling = this.element.previousElementSibling
|
198
|
+
return sibling?.tagName.match(/trix-toolbar/i) ? sibling : null
|
199
|
+
}
|
200
|
+
|
201
|
+
get inputElement() {
|
202
|
+
return document.getElementById(this.element.getAttribute('input'))
|
203
|
+
}
|
204
|
+
|
205
|
+
get formElement() {
|
206
|
+
return this.element.closest('form')
|
207
|
+
}
|
208
|
+
|
209
|
+
get paranoid() {
|
210
|
+
return !!this.store.read('paranoid')
|
211
|
+
}
|
212
|
+
|
213
|
+
get key() {
|
214
|
+
try {
|
215
|
+
return JSON.parse(this.store.read('key'))[2]
|
216
|
+
} catch {}
|
217
|
+
}
|
218
|
+
|
219
|
+
get hosts() {
|
220
|
+
try {
|
221
|
+
return decryptValues(this.key, JSON.parse(this.store.read('hosts')))
|
222
|
+
} catch {
|
223
|
+
return []
|
224
|
+
}
|
225
|
+
}
|
226
|
+
|
227
|
+
get reservedDomains() {
|
228
|
+
return ['example.com', 'test.com', 'invalid.com', 'example.cat', 'nic.example', 'example.co.uk']
|
229
|
+
}
|
230
|
+
|
231
|
+
async rememberConfig() {
|
232
|
+
const key = await generateKey()
|
233
|
+
const fakes = await encryptValues(key, this.reservedDomains)
|
234
|
+
const hosts = await encryptValues(key, this.hostsValue)
|
235
|
+
|
236
|
+
this.store.write('key', JSON.stringify([fakes[0], fakes[1], key, fakes[2]]))
|
237
|
+
this.element.removeAttribute('data-trix-embed-key-value')
|
238
|
+
|
239
|
+
this.store.write('hosts', JSON.stringify(hosts))
|
240
|
+
this.element.removeAttribute('data-trix-embed-hosts-value')
|
241
|
+
|
242
|
+
if (this.paranoidValue !== false) {
|
243
|
+
this.store.write('paranoid', JSON.stringify(fakes.slice(3)))
|
244
|
+
this.element.removeAttribute('data-trix-embed-paranoid')
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
forgetConfig() {
|
249
|
+
this.store.remove('key')
|
250
|
+
this.store.remove('hosts')
|
251
|
+
this.store.remove('paranoid')
|
252
|
+
}
|
253
|
+
}
|
254
|
+
}
|
@@ -0,0 +1,119 @@
|
|
1
|
+
const options = { name: 'AES-GCM', length: 256 } // encryption options
|
2
|
+
const extractable = true // makes it possible to export the key
|
3
|
+
const purposes = ['encrypt', 'decrypt']
|
4
|
+
|
5
|
+
// Generates a key for use with a symmetric encryption algorithm
|
6
|
+
//
|
7
|
+
// @returns {Promise<CryptoKey>} - The generated key
|
8
|
+
//
|
9
|
+
async function generateEncryptionKey() {
|
10
|
+
const extractable = true // makes it possible to export the key later
|
11
|
+
const purposes = ['encrypt', 'decrypt']
|
12
|
+
return await crypto.subtle.generateKey(options, extractable, purposes)
|
13
|
+
}
|
14
|
+
|
15
|
+
// Exports an encryption key
|
16
|
+
//
|
17
|
+
// @param {CryptoKey} key - The key to export
|
18
|
+
// @returns {Promise<String>} - The exported key as a JSON string
|
19
|
+
//
|
20
|
+
async function exportKey(key) {
|
21
|
+
const exported = await crypto.subtle.exportKey('jwk', key)
|
22
|
+
return JSON.stringify(exported)
|
23
|
+
}
|
24
|
+
|
25
|
+
// Imports an encryption key
|
26
|
+
//
|
27
|
+
// @param {String} key - The key to import as a string
|
28
|
+
// @returns {Promise<CryptoKey>} - The imported key
|
29
|
+
//
|
30
|
+
async function importKey(key) {
|
31
|
+
const parsed = JSON.parse(key)
|
32
|
+
return await crypto.subtle.importKey('jwk', parsed, options, extractable, purposes)
|
33
|
+
}
|
34
|
+
|
35
|
+
// Encrypts a value using a symmetric encryption algorithm
|
36
|
+
//
|
37
|
+
// @param {String} value - The value to encrypt
|
38
|
+
// @param {CryptoKey} key - The key to use for encryption
|
39
|
+
// @returns {Promise<String>} - Base64 encoded representation of the encrypted value
|
40
|
+
//
|
41
|
+
async function encrypt(value, key) {
|
42
|
+
const encoded = new TextEncoder().encode(String(value))
|
43
|
+
const iv = crypto.getRandomValues(new Uint8Array(12)) // initialization vector
|
44
|
+
const buffer = await crypto.subtle.encrypt({ ...options, iv }, key, encoded) // ciphertext as an ArrayBuffer
|
45
|
+
const data = {
|
46
|
+
ciphertext: btoa(String.fromCharCode(...new Uint8Array(buffer))),
|
47
|
+
iv: btoa(String.fromCharCode(...iv))
|
48
|
+
}
|
49
|
+
return btoa(JSON.stringify(data))
|
50
|
+
}
|
51
|
+
|
52
|
+
// Decrypts a value using a symmetric encryption algorithm
|
53
|
+
//
|
54
|
+
// @param {String} encrypted - The Base64 encoded encrypted value
|
55
|
+
// @param {CryptoKey} key - The key to use for decryption
|
56
|
+
// @returns {Promise<String>} - The decrypted value
|
57
|
+
//
|
58
|
+
async function decrypt(encrypted, key) {
|
59
|
+
const data = JSON.parse(atob(encrypted))
|
60
|
+
const ciphertextArray = new Uint8Array(
|
61
|
+
atob(data.ciphertext)
|
62
|
+
.split('')
|
63
|
+
.map(char => char.charCodeAt(0))
|
64
|
+
)
|
65
|
+
const iv = new Uint8Array(
|
66
|
+
atob(data.iv)
|
67
|
+
.split('')
|
68
|
+
.map(char => char.charCodeAt(0))
|
69
|
+
)
|
70
|
+
|
71
|
+
const buffer = await crypto.subtle.decrypt({ ...options, iv }, key, ciphertextArray)
|
72
|
+
return new TextDecoder().decode(buffer)
|
73
|
+
}
|
74
|
+
|
75
|
+
// Generates a new encryption key
|
76
|
+
//
|
77
|
+
// @returns {Promise<String>} - The base64 encoded key
|
78
|
+
//
|
79
|
+
export async function generateKey() {
|
80
|
+
const key = await generateEncryptionKey()
|
81
|
+
const jsonKey = await exportKey(key)
|
82
|
+
const base64Key = btoa(jsonKey)
|
83
|
+
return base64Key
|
84
|
+
}
|
85
|
+
|
86
|
+
// Encrypts a list of values
|
87
|
+
//
|
88
|
+
// @param {String} base64Key - The encryption key to use
|
89
|
+
// @param {String[]} values - The values to encrypt
|
90
|
+
// @returns {Promise<String>[]} - The encrypted values
|
91
|
+
//
|
92
|
+
export async function encryptValues(base64Key, values = []) {
|
93
|
+
const key = await importKey(atob(base64Key))
|
94
|
+
return Promise.all(values.map(value => encrypt(value, key)))
|
95
|
+
}
|
96
|
+
|
97
|
+
// Decrypts and logs a list of values
|
98
|
+
//
|
99
|
+
// @param {String} base64Key - The encryption key to use
|
100
|
+
// @param {String[]} values - The values to decrypt
|
101
|
+
// @returns {Promise<String>[]} - The decrypted values
|
102
|
+
//
|
103
|
+
export async function decryptValues(base64Key, encryptedValues = []) {
|
104
|
+
const key = await importKey(atob(base64Key))
|
105
|
+
return Promise.all(encryptedValues.map(encryptedValue => decrypt(encryptedValue, key)))
|
106
|
+
}
|
107
|
+
|
108
|
+
// Generates a new encryption key and encrypts a list of values
|
109
|
+
//
|
110
|
+
// @param {Array} values - The values to encrypt
|
111
|
+
// @returns {Promise<Object>} - The encryption key and encrypted values
|
112
|
+
//
|
113
|
+
export async function generateKeyAndEncryptValues(values = []) {
|
114
|
+
const key = await generateKey()
|
115
|
+
const encryptedValues = await encryptValues(key, values)
|
116
|
+
console.log(`data-trix-embed-key-value="${key}"`)
|
117
|
+
console.log(`data-trix-embed-hosts-value='${JSON.stringify(encryptedValues)}'`)
|
118
|
+
return { key, encryptedValues }
|
119
|
+
}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
const submitGuards = {}
|
2
|
+
|
3
|
+
export default class Guard {
|
4
|
+
constructor(controller) {
|
5
|
+
this.controller = controller
|
6
|
+
controller.element.addEventListener('trix-file-accept', event => event.preventDefault())
|
7
|
+
}
|
8
|
+
|
9
|
+
protectSubmit = event => {
|
10
|
+
const form = this.controller.formElement
|
11
|
+
const f = event.target.closest('form')
|
12
|
+
if (f && f.action === form.action && f.method === form.method && f !== form) event.preventDefault()
|
13
|
+
}
|
14
|
+
|
15
|
+
protect() {
|
16
|
+
if (!this.controller.formElement) return
|
17
|
+
const form = this.controller.formElement
|
18
|
+
const input = this.controller.inputElement
|
19
|
+
const key = `${form.method}${form.action}`
|
20
|
+
|
21
|
+
document.removeEventListener('submit', handlers[key], true)
|
22
|
+
handlers[key] = this.protectSubmit.bind(this)
|
23
|
+
document.addEventListener('submit', handlers[key], true)
|
24
|
+
|
25
|
+
const observer = new MutationObserver((mutations, observer) => {
|
26
|
+
mutations.forEach(mutation => {
|
27
|
+
const { addedNodes, target, type } = mutation
|
28
|
+
|
29
|
+
switch (type) {
|
30
|
+
case 'attributes':
|
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
|
+
})
|
46
|
+
|
47
|
+
observer.observe(document.body, {
|
48
|
+
attributeFilter: ['id', 'name'],
|
49
|
+
attributes: true,
|
50
|
+
childList: true,
|
51
|
+
subtree: true
|
52
|
+
})
|
53
|
+
}
|
54
|
+
|
55
|
+
cleanup() {
|
56
|
+
const trix = this.controller.element
|
57
|
+
const input = this.controller.inputElement
|
58
|
+
const toolbar = this.controller.toolbarElement
|
59
|
+
|
60
|
+
input?.remove()
|
61
|
+
toolbar?.remove()
|
62
|
+
trix?.remove()
|
63
|
+
}
|
64
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { generateKey, encryptValues, generateKeyAndEncryptValues } from './encryption'
|
2
|
+
import { getTrixEmbedControllerClass } from './controller'
|
3
|
+
|
4
|
+
const defaultOptions = {
|
5
|
+
application: null,
|
6
|
+
Controller: null,
|
7
|
+
Trix: null
|
8
|
+
}
|
9
|
+
|
10
|
+
function initialize(options = defaultOptions) {
|
11
|
+
const { application, Controller, Trix } = options
|
12
|
+
application.register('trix-embed', getTrixEmbedControllerClass({ Controller, Trix }))
|
13
|
+
}
|
14
|
+
|
15
|
+
self.TrixEmbed = {
|
16
|
+
initialize,
|
17
|
+
generateKey,
|
18
|
+
encryptValues,
|
19
|
+
generateKeyAndEncryptValues
|
20
|
+
}
|
21
|
+
|
22
|
+
export default self.TrixEmbed
|