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
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
|