Some fixes and optimizations in sandboxes.
Start of a sandbox for GrapeJS
This commit is contained in:
parent
02a7275ae4
commit
e2093e22fe
22 changed files with 742 additions and 294 deletions
|
@ -24,6 +24,7 @@ const reports = require('./routes/reports');
|
||||||
const subscription = require('./routes/subscription');
|
const subscription = require('./routes/subscription');
|
||||||
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
|
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
|
||||||
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
|
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
|
||||||
|
const sandboxedGrapesJS = require('./routes/sandboxed-grapesjs');
|
||||||
const files = require('./routes/files');
|
const files = require('./routes/files');
|
||||||
const links = require('./routes/links');
|
const links = require('./routes/links');
|
||||||
const archive = require('./routes/archive');
|
const archive = require('./routes/archive');
|
||||||
|
@ -224,6 +225,7 @@ function createApp(appType) {
|
||||||
|
|
||||||
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
|
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
|
||||||
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
|
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
|
||||||
|
useWith404Fallback('/grapesjs', sandboxedGrapesJS.getRouter(appType));
|
||||||
|
|
||||||
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
|
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
|
||||||
if (config.reports && config.reports.enabled === true) {
|
if (config.reports && config.reports.enabled === true) {
|
||||||
|
|
10
client/package-lock.json
generated
10
client/package-lock.json
generated
|
@ -3837,6 +3837,16 @@
|
||||||
"ua-parser-js": "0.7.17"
|
"ua-parser-js": "0.7.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"file-loader": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"loader-utils": "1.1.0",
|
||||||
|
"schema-utils": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"filename-regex": {
|
"filename-regex": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
|
||||||
|
|
|
@ -71,6 +71,7 @@
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
"babel-preset-stage-1": "^6.24.1",
|
"babel-preset-stage-1": "^6.24.1",
|
||||||
"css-loader": "^0.28.4",
|
"css-loader": "^0.28.4",
|
||||||
|
"file-loader": "^2.0.0",
|
||||||
"i18next-conv": "^3.0.3",
|
"i18next-conv": "^3.0.3",
|
||||||
"node-sass": "^4.5.3",
|
"node-sass": "^4.5.3",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
|
||||||
|
|
||||||
import 'grapesjs/dist/css/grapes.min.css';
|
|
||||||
import grapesjs from 'grapesjs';
|
|
||||||
import grapesjsMjml from 'grapesjs-mjml';
|
|
||||||
|
|
||||||
export default class GrapesJs extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const editor = grapesjs.init({
|
|
||||||
container: this.canvasNode,
|
|
||||||
height: '100%',
|
|
||||||
width: 'auto',
|
|
||||||
noticeOnUnload: 0,
|
|
||||||
storageManager:{autoload: 0},
|
|
||||||
fromElement: false,
|
|
||||||
components:
|
|
||||||
' <mj-container>\n' +
|
|
||||||
' <mj-section>\n' +
|
|
||||||
' <mj-column>\n' +
|
|
||||||
' <mj-text>My Company</mj-text>\n' +
|
|
||||||
' </mj-column>\n' +
|
|
||||||
' </mj-section>\n' +
|
|
||||||
' <mj-container>',
|
|
||||||
plugins: ['gjs-mjml'],
|
|
||||||
pluginsOpts: {
|
|
||||||
'gjs-mjml': {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div ref={node => this.canvasNode = node}/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
.gjs-cv-canvas {
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
import {getUrl} from "./urls";
|
import {getUrl} from "./urls";
|
||||||
|
|
||||||
__webpack_public_path__ = getUrl();
|
__webpack_public_path__ = getUrl('mailtrain/');
|
||||||
|
|
|
@ -2,12 +2,133 @@
|
||||||
|
|
||||||
import './public-path';
|
import './public-path';
|
||||||
|
|
||||||
import React from 'react';
|
import React, {Component} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM
|
||||||
import {I18nextProvider,} from 'react-i18next';
|
from 'react-dom';
|
||||||
import i18n from './i18n';
|
import {
|
||||||
import {CKEditorSandbox} from './sandboxed-ckeditor';
|
I18nextProvider,
|
||||||
import {UntrustedContentRoot, parentRPC} from './untrusted';
|
translate,
|
||||||
|
} from 'react-i18next';
|
||||||
|
import i18n
|
||||||
|
from './i18n';
|
||||||
|
import {
|
||||||
|
parentRPC,
|
||||||
|
UntrustedContentRoot
|
||||||
|
} from './untrusted';
|
||||||
|
import PropTypes
|
||||||
|
from "prop-types";
|
||||||
|
import styles
|
||||||
|
from "./sandboxed-ckeditor.scss";
|
||||||
|
import {
|
||||||
|
getPublicUrl,
|
||||||
|
getSandboxUrl,
|
||||||
|
getTrustedUrl
|
||||||
|
} from "./urls";
|
||||||
|
import {
|
||||||
|
base,
|
||||||
|
unbase
|
||||||
|
} from "../../../shared/templates";
|
||||||
|
|
||||||
|
import CKEditor
|
||||||
|
from "react-ckeditor-component";
|
||||||
|
|
||||||
|
import { initialHeight } from "./sandboxed-ckeditor-shared";
|
||||||
|
|
||||||
|
|
||||||
|
@translate(null, { withRef: true })
|
||||||
|
class CKEditorSandbox extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const trustedUrlBase = getTrustedUrl();
|
||||||
|
const sandboxUrlBase = getSandboxUrl();
|
||||||
|
const publicUrlBase = getPublicUrl();
|
||||||
|
const html = this.props.initialHtml && base(this.props.initialHtml, trustedUrlBase, sandboxUrlBase, publicUrlBase);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
html
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
entityTypeId: PropTypes.string,
|
||||||
|
entityId: PropTypes.number,
|
||||||
|
initialHtml: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportState(method, params) {
|
||||||
|
const trustedUrlBase = getTrustedUrl();
|
||||||
|
const sandboxUrlBase = getSandboxUrl();
|
||||||
|
const publicUrlBase = getPublicUrl();
|
||||||
|
return {
|
||||||
|
html: unbase(this.state.html, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async setHeight(methods, params) {
|
||||||
|
this.node.editorInstance.resize('100%', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
parentRPC.setMethodHandler('exportState', ::this.exportState);
|
||||||
|
parentRPC.setMethodHandler('setHeight', ::this.setHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const config = {
|
||||||
|
toolbarGroups: [
|
||||||
|
{
|
||||||
|
name: "document",
|
||||||
|
groups: ["document", "doctools"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clipboard",
|
||||||
|
groups: ["clipboard", "undo"]
|
||||||
|
},
|
||||||
|
{name: "styles"},
|
||||||
|
{
|
||||||
|
name: "basicstyles",
|
||||||
|
groups: ["basicstyles", "cleanup"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "editing",
|
||||||
|
groups: ["find", "selection", "spellchecker"]
|
||||||
|
},
|
||||||
|
{name: "forms"},
|
||||||
|
{
|
||||||
|
name: "paragraph",
|
||||||
|
groups: ["list",
|
||||||
|
"indent", "blocks", "align", "bidi"]
|
||||||
|
},
|
||||||
|
{name: "links"},
|
||||||
|
{name: "insert"},
|
||||||
|
{name: "colors"},
|
||||||
|
{name: "tools"},
|
||||||
|
{name: "others"},
|
||||||
|
{
|
||||||
|
name: "document-mode",
|
||||||
|
groups: ["mode"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
removeButtons: 'Underline,Subscript,Superscript,Maximize',
|
||||||
|
resize_enabled: false,
|
||||||
|
height: initialHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.sandbox}>
|
||||||
|
<CKEditor ref={node => this.node = node}
|
||||||
|
content={this.state.html}
|
||||||
|
events={{
|
||||||
|
change: evt => this.setState({html: evt.editor.getData()}),
|
||||||
|
}}
|
||||||
|
config={config}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
parentRPC.init();
|
parentRPC.init();
|
||||||
|
|
3
client/src/lib/sandboxed-ckeditor-shared.js
Normal file
3
client/src/lib/sandboxed-ckeditor-shared.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
export const initialHeight = 600;
|
|
@ -2,23 +2,16 @@
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import {translate} from 'react-i18next';
|
import {translate} from 'react-i18next';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes
|
||||||
import styles from "./sandboxed-ckeditor.scss";
|
from "prop-types";
|
||||||
|
import styles
|
||||||
|
from "./sandboxed-ckeditor.scss";
|
||||||
|
|
||||||
import {UntrustedContentHost, parentRPC} from './untrusted';
|
import {UntrustedContentHost} from './untrusted';
|
||||||
import {Icon} from "./bootstrap-components";
|
import {Icon} from "./bootstrap-components";
|
||||||
import {
|
import {getTrustedUrl} from "./urls";
|
||||||
getPublicUrl,
|
|
||||||
getSandboxUrl,
|
|
||||||
getTrustedUrl
|
|
||||||
} from "./urls";
|
|
||||||
import {
|
|
||||||
base,
|
|
||||||
unbase
|
|
||||||
} from "../../../shared/templates";
|
|
||||||
import CKEditor from "react-ckeditor-component";
|
|
||||||
|
|
||||||
const initialHeight = 600;
|
import { initialHeight } from "./sandboxed-ckeditor-shared";
|
||||||
const navbarHeight = 34; // Sync this with navbarheight in sandboxed-ckeditor.scss
|
const navbarHeight = 34; // Sync this with navbarheight in sandboxed-ckeditor.scss
|
||||||
|
|
||||||
@translate(null, { withRef: true })
|
@translate(null, { withRef: true })
|
||||||
|
@ -108,62 +101,3 @@ CKEditorHost.prototype.exportState = async function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@translate(null, { withRef: true })
|
|
||||||
export class CKEditorSandbox extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const trustedUrlBase = getTrustedUrl();
|
|
||||||
const sandboxUrlBase = getSandboxUrl();
|
|
||||||
const publicUrlBase = getPublicUrl();
|
|
||||||
const html = this.props.initialHtml && base(this.props.initialHtml, trustedUrlBase, sandboxUrlBase, publicUrlBase);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
html
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
entityTypeId: PropTypes.string,
|
|
||||||
entityId: PropTypes.number,
|
|
||||||
initialHtml: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportState(method, params) {
|
|
||||||
const trustedUrlBase = getTrustedUrl();
|
|
||||||
const sandboxUrlBase = getSandboxUrl();
|
|
||||||
const publicUrlBase = getPublicUrl();
|
|
||||||
return {
|
|
||||||
html: unbase(this.state.html, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async setHeight(methods, params) {
|
|
||||||
this.node.editorInstance.resize('100%', params);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
parentRPC.setMethodHandler('exportState', ::this.exportState);
|
|
||||||
parentRPC.setMethodHandler('setHeight', ::this.setHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const config = {
|
|
||||||
removeButtons: 'Underline,Subscript,Superscript,Maximize',
|
|
||||||
resize_enabled: false,
|
|
||||||
height: initialHeight
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.sandbox}>
|
|
||||||
<CKEditor ref={node => this.node = node}
|
|
||||||
content={this.state.html}
|
|
||||||
events={{
|
|
||||||
change: evt => this.setState({html: evt.editor.getData()}),
|
|
||||||
}}
|
|
||||||
config={config}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
157
client/src/lib/sandboxed-grapesjs-root.js
Normal file
157
client/src/lib/sandboxed-grapesjs-root.js
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import './public-path';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import ReactDOM
|
||||||
|
from 'react-dom';
|
||||||
|
import {
|
||||||
|
I18nextProvider,
|
||||||
|
translate,
|
||||||
|
} from 'react-i18next';
|
||||||
|
import i18n
|
||||||
|
from './i18n';
|
||||||
|
import {
|
||||||
|
parentRPC,
|
||||||
|
UntrustedContentRoot
|
||||||
|
} from './untrusted';
|
||||||
|
import PropTypes
|
||||||
|
from "prop-types";
|
||||||
|
import {
|
||||||
|
getPublicUrl,
|
||||||
|
getSandboxUrl,
|
||||||
|
getTrustedUrl
|
||||||
|
} from "./urls";
|
||||||
|
import {
|
||||||
|
base,
|
||||||
|
unbase
|
||||||
|
} from "../../../shared/templates";
|
||||||
|
|
||||||
|
import 'grapesjs/dist/css/grapes.min.css';
|
||||||
|
import grapesjs from 'grapesjs';
|
||||||
|
import "grapesjs-mjml";
|
||||||
|
|
||||||
|
import "./sandboxed-grapesjs.scss";
|
||||||
|
|
||||||
|
grapesjs.plugins.add('mailtrain', (editor, opts = {}) => {
|
||||||
|
const panelManager = editor.Panels;
|
||||||
|
panelManager.removeButton('options','fullscreen')
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@translate(null, { withRef: true })
|
||||||
|
export class GrapesJSSandbox extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
entityTypeId: PropTypes.string,
|
||||||
|
entityId: PropTypes.number,
|
||||||
|
initialModel: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportState(method, params) {
|
||||||
|
const editor = this.editor;
|
||||||
|
|
||||||
|
const trustedUrlBase = getTrustedUrl();
|
||||||
|
const sandboxUrlBase = getSandboxUrl();
|
||||||
|
const publicUrlBase = getPublicUrl();
|
||||||
|
|
||||||
|
let html;
|
||||||
|
html = unbase(editor.getHtml(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true);
|
||||||
|
|
||||||
|
const model = {
|
||||||
|
css: editor.getCss(),
|
||||||
|
source: editor.getHtml(),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(model.css);
|
||||||
|
console.log(model.source);
|
||||||
|
|
||||||
|
return {
|
||||||
|
html,
|
||||||
|
model
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
parentRPC.setMethodHandler('exportState', ::this.exportState);
|
||||||
|
|
||||||
|
const trustedUrlBase = getTrustedUrl();
|
||||||
|
const sandboxUrlBase = getSandboxUrl();
|
||||||
|
const publicUrlBase = getPublicUrl();
|
||||||
|
|
||||||
|
const model = this.props.initialModel || {}
|
||||||
|
|
||||||
|
const source = model.source && base(model.source, trustedUrlBase, sandboxUrlBase, publicUrlBase);
|
||||||
|
const css = model.css && base(model.css, trustedUrlBase, sandboxUrlBase, publicUrlBase);
|
||||||
|
|
||||||
|
/*
|
||||||
|
' <mj-container>\n' +
|
||||||
|
' <mj-section>\n' +
|
||||||
|
' <mj-column>\n' +
|
||||||
|
' <mj-text>My Company</mj-text>\n' +
|
||||||
|
' </mj-column>\n' +
|
||||||
|
' </mj-section>\n' +
|
||||||
|
' <mj-container>',
|
||||||
|
*/
|
||||||
|
|
||||||
|
this.editor = grapesjs.init({
|
||||||
|
container: this.canvasNode,
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
storageManager:{
|
||||||
|
type: 'none'
|
||||||
|
},
|
||||||
|
assetManager: {
|
||||||
|
assets: [],
|
||||||
|
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}',
|
||||||
|
uploadText: 'Drop images here or click to upload',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-TOKEN': '{{csrfToken}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
styleManager: {
|
||||||
|
clearProperties: true,
|
||||||
|
},
|
||||||
|
fromElement: false,
|
||||||
|
components: source,
|
||||||
|
style: css,
|
||||||
|
plugins: [
|
||||||
|
'mailtrain',
|
||||||
|
'gjs-mjml'
|
||||||
|
],
|
||||||
|
pluginsOpts: {
|
||||||
|
'gjs-mjml': {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div ref={node => this.canvasNode = node}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
parentRPC.init();
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<I18nextProvider i18n={ i18n }>
|
||||||
|
<UntrustedContentRoot render={props => <GrapesJSSandbox {...props} />} />
|
||||||
|
</I18nextProvider>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
73
client/src/lib/sandboxed-grapesjs.js
Normal file
73
client/src/lib/sandboxed-grapesjs.js
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import React, {Component} from 'react';
|
||||||
|
import {translate} from 'react-i18next';
|
||||||
|
import PropTypes
|
||||||
|
from "prop-types";
|
||||||
|
import styles
|
||||||
|
from "./sandboxed-grapesjs.scss";
|
||||||
|
|
||||||
|
import {UntrustedContentHost} from './untrusted';
|
||||||
|
import {Icon} from "./bootstrap-components";
|
||||||
|
import {getTrustedUrl} from "./urls";
|
||||||
|
|
||||||
|
@translate(null, { withRef: true })
|
||||||
|
export class GrapesJSHost extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fullscreen: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
entityTypeId: PropTypes.string,
|
||||||
|
entity: PropTypes.object,
|
||||||
|
initialModel: PropTypes.object,
|
||||||
|
title: PropTypes.string,
|
||||||
|
onFullscreenAsync: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleFullscreenAsync() {
|
||||||
|
const fullscreen = !this.state.fullscreen;
|
||||||
|
this.setState({
|
||||||
|
fullscreen
|
||||||
|
});
|
||||||
|
await this.props.onFullscreenAsync(fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportState() {
|
||||||
|
return await this.contentNode.ask('exportState');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const t = this.props.t;
|
||||||
|
|
||||||
|
const editorData = {
|
||||||
|
entityTypeId: this.props.entityTypeId,
|
||||||
|
entityId: this.props.entity.id,
|
||||||
|
initialModel: this.props.initialModel
|
||||||
|
};
|
||||||
|
|
||||||
|
const tokenData = {
|
||||||
|
entityTypeId: this.props.entityTypeId,
|
||||||
|
entityId: this.props.entity.id
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
|
||||||
|
<div className={styles.navbar}>
|
||||||
|
{this.state.fullscreen && <img className={styles.logo} src={getTrustedUrl('static/mailtrain-notext.png')}/>}
|
||||||
|
<div className={styles.title}>{this.props.title}</div>
|
||||||
|
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
|
||||||
|
</div>
|
||||||
|
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="grapesjs/editor" tokenMethod="grapesjs" tokenParams={tokenData}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GrapesJSHost.prototype.exportState = async function() {
|
||||||
|
return await this.getWrappedInstance().exportState();
|
||||||
|
};
|
86
client/src/lib/sandboxed-grapesjs.scss
Normal file
86
client/src/lib/sandboxed-grapesjs.scss
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
$navbarHeight: 34px;
|
||||||
|
|
||||||
|
.editor {
|
||||||
|
.host {
|
||||||
|
height: 800px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorFullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: white;
|
||||||
|
margin-top: $navbarHeight;
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
margin-top: -$navbarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background: #DE4320;
|
||||||
|
width: 100%;
|
||||||
|
height: $navbarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
float: left;
|
||||||
|
height: $navbarHeight;
|
||||||
|
padding: 5px 0 5px 10px;
|
||||||
|
filter: brightness(0) invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
padding: 5px 0 5px 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-family: "Ubuntu",Tahoma,"Helvetica Neue",Helvetica,Arial,sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
float: left;
|
||||||
|
color: white;
|
||||||
|
height: $navbarHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
padding: 0px 15px;
|
||||||
|
line-height: $navbarHeight;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #b1381e;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:global .grapesjs-body {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .gjs-editor-cont {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .gjs-devices-c .gjs-devices {
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global .gjs-pn-devices-c, :global .gjs-pn-views {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,120 @@
|
||||||
|
|
||||||
import './public-path';
|
import './public-path';
|
||||||
|
|
||||||
import React from 'react';
|
import React, {Component} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM
|
||||||
import {I18nextProvider,} from 'react-i18next';
|
from 'react-dom';
|
||||||
import i18n from './i18n';
|
import {
|
||||||
import {MosaicoSandbox} from './sandboxed-mosaico';
|
I18nextProvider,
|
||||||
import {UntrustedContentRoot, parentRPC} from './untrusted';
|
translate,
|
||||||
|
} from 'react-i18next';
|
||||||
|
import i18n
|
||||||
|
from './i18n';
|
||||||
|
import {
|
||||||
|
parentRPC,
|
||||||
|
UntrustedContentRoot
|
||||||
|
} from './untrusted';
|
||||||
|
import PropTypes
|
||||||
|
from "prop-types";
|
||||||
|
import {
|
||||||
|
getPublicUrl,
|
||||||
|
getSandboxUrl,
|
||||||
|
getTrustedUrl
|
||||||
|
} from "./urls";
|
||||||
|
import {
|
||||||
|
base,
|
||||||
|
unbase
|
||||||
|
} from "../../../shared/templates";
|
||||||
|
|
||||||
|
|
||||||
|
@translate(null, { withRef: true })
|
||||||
|
class MosaicoSandbox extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.viewModel = null;
|
||||||
|
this.state = {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
entityTypeId: PropTypes.string,
|
||||||
|
entityId: PropTypes.number,
|
||||||
|
templateId: PropTypes.number,
|
||||||
|
templatePath: PropTypes.string,
|
||||||
|
initialModel: PropTypes.string,
|
||||||
|
initialMetadata: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportState(method, params) {
|
||||||
|
const trustedUrlBase = getTrustedUrl();
|
||||||
|
const sandboxUrlBase = getSandboxUrl();
|
||||||
|
const publicUrlBase = getPublicUrl();
|
||||||
|
return {
|
||||||
|
html: unbase(this.viewModel.exportHTML(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true),
|
||||||
|
model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase, publicUrlBase),
|
||||||
|
metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase, publicUrlBase)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
parentRPC.setMethodHandler('exportState', ::this.exportState);
|
||||||
|
|
||||||
|
if (!Mosaico.isCompatible()) {
|
||||||
|
alert('Update your browser!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugins = [...window.mosaicoPlugins];
|
||||||
|
|
||||||
|
plugins.push(viewModel => {
|
||||||
|
this.viewModel = viewModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
// (Custom) HTML postRenderers
|
||||||
|
plugins.push(viewModel => {
|
||||||
|
viewModel.originalExportHTML = viewModel.exportHTML;
|
||||||
|
viewModel.exportHTML = () => {
|
||||||
|
let html = viewModel.originalExportHTML();
|
||||||
|
for (const portRender of window.mosaicoHTMLPostRenderers) {
|
||||||
|
html = postRender(html);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
plugins.unshift(vm => {
|
||||||
|
// This is an override of the default paths in Mosaico
|
||||||
|
vm.logoPath = getTrustedUrl('static/mosaico/img/mosaico32.png');
|
||||||
|
vm.logoUrl = '#';
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
imgProcessorBackend: getTrustedUrl('mosaico/img'),
|
||||||
|
emailProcessorBackend: getSandboxUrl('mosaico/dl'),
|
||||||
|
fileuploadConfig: {
|
||||||
|
url: getSandboxUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
|
||||||
|
},
|
||||||
|
strings: window.mosaicoLanguageStrings
|
||||||
|
};
|
||||||
|
|
||||||
|
const trustedUrlBase = getTrustedUrl();
|
||||||
|
const sandboxUrlBase = getSandboxUrl();
|
||||||
|
const publicUrlBase = getPublicUrl();
|
||||||
|
const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase, publicUrlBase));
|
||||||
|
const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, trustedUrlBase, sandboxUrlBase, publicUrlBase));
|
||||||
|
const template = this.props.templateId ? getSandboxUrl(`mosaico/templates/${this.props.templateId}/index.html`) : this.props.templatePath;
|
||||||
|
|
||||||
|
const allPlugins = plugins.concat(window.mosaicoPlugins);
|
||||||
|
|
||||||
|
Mosaico.start(config, template, metadata, model, allPlugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <div/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
parentRPC.init();
|
parentRPC.init();
|
||||||
|
|
|
@ -2,24 +2,18 @@
|
||||||
|
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import {translate} from 'react-i18next';
|
import {translate} from 'react-i18next';
|
||||||
import PropTypes from "prop-types";
|
import PropTypes
|
||||||
import styles from "./sandboxed-mosaico.scss";
|
from "prop-types";
|
||||||
|
import styles
|
||||||
|
from "./sandboxed-mosaico.scss";
|
||||||
|
|
||||||
import {UntrustedContentHost, parentRPC} from './untrusted';
|
import {UntrustedContentHost} from './untrusted';
|
||||||
import {Icon} from "./bootstrap-components";
|
import {Icon} from "./bootstrap-components";
|
||||||
import {
|
import {getTrustedUrl} from "./urls";
|
||||||
getPublicUrl,
|
|
||||||
getSandboxUrl,
|
|
||||||
getTrustedUrl
|
|
||||||
} from "./urls";
|
|
||||||
import {
|
|
||||||
base,
|
|
||||||
unbase
|
|
||||||
} from "../../../shared/templates";
|
|
||||||
|
|
||||||
|
|
||||||
@translate(null, { withRef: true })
|
@translate(null, { withRef: true })
|
||||||
export class MosaicoEditorHost extends Component {
|
export class MosaicoHost extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -81,96 +75,7 @@ export class MosaicoEditorHost extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MosaicoEditorHost.prototype.exportState = async function() {
|
MosaicoHost.prototype.exportState = async function() {
|
||||||
return await this.getWrappedInstance().exportState();
|
return await this.getWrappedInstance().exportState();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@translate(null, { withRef: true })
|
|
||||||
export class MosaicoSandbox extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.viewModel = null;
|
|
||||||
this.state = {
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
entityTypeId: PropTypes.string,
|
|
||||||
entityId: PropTypes.number,
|
|
||||||
templateId: PropTypes.number,
|
|
||||||
templatePath: PropTypes.string,
|
|
||||||
initialModel: PropTypes.string,
|
|
||||||
initialMetadata: PropTypes.string
|
|
||||||
}
|
|
||||||
|
|
||||||
async exportState(method, params) {
|
|
||||||
const trustedUrlBase = getTrustedUrl();
|
|
||||||
const sandboxUrlBase = getSandboxUrl();
|
|
||||||
const publicUrlBase = getPublicUrl();
|
|
||||||
return {
|
|
||||||
html: unbase(this.viewModel.exportHTML(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true),
|
|
||||||
model: unbase(this.viewModel.exportJSON(), trustedUrlBase, sandboxUrlBase, publicUrlBase),
|
|
||||||
metadata: unbase(this.viewModel.exportMetadata(), trustedUrlBase, sandboxUrlBase, publicUrlBase)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
parentRPC.setMethodHandler('exportState', ::this.exportState);
|
|
||||||
|
|
||||||
if (!Mosaico.isCompatible()) {
|
|
||||||
alert('Update your browser!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugins = [...window.mosaicoPlugins];
|
|
||||||
|
|
||||||
plugins.push(viewModel => {
|
|
||||||
this.viewModel = viewModel;
|
|
||||||
});
|
|
||||||
|
|
||||||
// (Custom) HTML postRenderers
|
|
||||||
plugins.push(viewModel => {
|
|
||||||
viewModel.originalExportHTML = viewModel.exportHTML;
|
|
||||||
viewModel.exportHTML = () => {
|
|
||||||
let html = viewModel.originalExportHTML();
|
|
||||||
for (const portRender of window.mosaicoHTMLPostRenderers) {
|
|
||||||
html = postRender(html);
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
plugins.unshift(vm => {
|
|
||||||
// This is an override of the default paths in Mosaico
|
|
||||||
vm.logoPath = getTrustedUrl('static/mosaico/img/mosaico32.png');
|
|
||||||
vm.logoUrl = '#';
|
|
||||||
});
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
imgProcessorBackend: getTrustedUrl('mosaico/img'),
|
|
||||||
emailProcessorBackend: getSandboxUrl('mosaico/dl'),
|
|
||||||
fileuploadConfig: {
|
|
||||||
url: getSandboxUrl(`mosaico/upload/${this.props.entityTypeId}/${this.props.entityId}`)
|
|
||||||
},
|
|
||||||
strings: window.mosaicoLanguageStrings
|
|
||||||
};
|
|
||||||
|
|
||||||
const trustedUrlBase = getTrustedUrl();
|
|
||||||
const sandboxUrlBase = getSandboxUrl();
|
|
||||||
const publicUrlBase = getPublicUrl();
|
|
||||||
const metadata = this.props.initialMetadata && JSON.parse(base(this.props.initialMetadata, trustedUrlBase, sandboxUrlBase, publicUrlBase));
|
|
||||||
const model = this.props.initialModel && JSON.parse(base(this.props.initialModel, trustedUrlBase, sandboxUrlBase, publicUrlBase));
|
|
||||||
const template = this.props.templateId ? getSandboxUrl(`mosaico/templates/${this.props.templateId}/index.html`) : this.props.templatePath;
|
|
||||||
|
|
||||||
const allPlugins = plugins.concat(window.mosaicoPlugins);
|
|
||||||
|
|
||||||
Mosaico.start(config, template, metadata, model, allPlugins);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div/>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,9 @@ import {
|
||||||
import 'brace/mode/text';
|
import 'brace/mode/text';
|
||||||
import 'brace/mode/html';
|
import 'brace/mode/html';
|
||||||
|
|
||||||
import { MosaicoEditorHost } from "../lib/sandboxed-mosaico";
|
import { MosaicoHost } from "../lib/sandboxed-mosaico";
|
||||||
import { CKEditorHost } from "../lib/sandboxed-ckeditor";
|
import { CKEditorHost } from "../lib/sandboxed-ckeditor";
|
||||||
import GrapesJS from "../lib/grapesjs";
|
import { GrapesJSHost } from "../lib/sandboxed-grapesjs";
|
||||||
|
|
||||||
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
|
||||||
import {getSandboxUrl} from "../lib/urls";
|
import {getSandboxUrl} from "../lib/urls";
|
||||||
|
@ -70,7 +70,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
<TableSelect id={prefix + 'mosaicoTemplate'} label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
|
<TableSelect id={prefix + 'mosaicoTemplate'} label={t('Mosaico template')} withHeader dropdown dataUrl='rest/mosaico-templates-table' columns={mosaicoTemplatesColumns} selectionLabelIndex={1} disabled={isEdit} />,
|
||||||
getHTMLEditor: owner =>
|
getHTMLEditor: owner =>
|
||||||
<AlignedRow label={t('Template content (HTML)')}>
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
<MosaicoEditorHost
|
<MosaicoHost
|
||||||
ref={node => owner.editorNode = node}
|
ref={node => owner.editorNode = node}
|
||||||
entity={owner.props.entity}
|
entity={owner.props.entity}
|
||||||
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||||
|
@ -132,7 +132,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
},
|
},
|
||||||
getHTMLEditor: owner =>
|
getHTMLEditor: owner =>
|
||||||
<AlignedRow label={t('Template content (HTML)')}>
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
<MosaicoEditorHost
|
<MosaicoHost
|
||||||
ref={node => owner.editorNode = node}
|
ref={node => owner.editorNode = node}
|
||||||
entity={owner.props.entity}
|
entity={owner.props.entity}
|
||||||
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
initialModel={owner.getFormValue(prefix + 'mosaicoData').model}
|
||||||
|
@ -175,38 +175,33 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
validate: state => {}
|
validate: state => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
templateTypes.xgrapesjs = { // FIXME
|
|
||||||
typeName: t('GrapeJS'),
|
|
||||||
getTypeForm: (owner, isEdit) => null,
|
|
||||||
getHTMLEditor: owner => null,
|
|
||||||
exportHTMLEditorData: async owner => {},
|
|
||||||
initData: () => ({}),
|
|
||||||
afterLoad: data => {},
|
|
||||||
beforeSave: data => {
|
|
||||||
clearBeforeSave(data);
|
|
||||||
},
|
|
||||||
afterTypeChange: mutState => {},
|
|
||||||
validate: state => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
templateTypes.grapesjs = {
|
templateTypes.grapesjs = {
|
||||||
typeName: t('GrapeJS (fake)'),
|
typeName: t('GrapeJS'),
|
||||||
getTypeForm: (owner, isEdit) => null,
|
getTypeForm: (owner, isEdit) => null,
|
||||||
getHTMLEditor: owner =>
|
getHTMLEditor: owner =>
|
||||||
<AlignedRow label={t('Template content (HTML)')}>
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
<GrapesJS
|
<GrapesJSHost
|
||||||
ref={node => owner.editorNode = node}
|
ref={node => owner.editorNode = node}
|
||||||
entity={owner.props.entity}
|
entity={owner.props.entity}
|
||||||
initialHtml={owner.getFormValue(prefix + 'html')}
|
initialModel={owner.getFormValue(prefix + 'grapesJSData')}
|
||||||
entityTypeId={entityTypeId}/>
|
entityTypeId={entityTypeId}
|
||||||
|
title={t('GrapesJS Template Designer')}
|
||||||
|
onFullscreenAsync={::owner.setElementInFullscreen}
|
||||||
|
/>
|
||||||
</AlignedRow>,
|
</AlignedRow>,
|
||||||
exportHTMLEditorData: async owner => {
|
exportHTMLEditorData: async owner => {
|
||||||
const {html} = await owner.editorNode.exportState();
|
const {html, model} = await owner.editorNode.exportState();
|
||||||
owner.updateFormValue(prefix + 'html', html);
|
owner.updateFormValue(prefix + 'html', html);
|
||||||
|
owner.updateFormValue(prefix + 'grapesJSData', model);
|
||||||
|
},
|
||||||
|
initData: () => ({
|
||||||
|
[prefix + 'grapesJSData']: {}
|
||||||
|
}),
|
||||||
|
afterLoad: data => {
|
||||||
|
data[prefix + 'grapesJSData'] = data[prefix + 'data'];
|
||||||
},
|
},
|
||||||
initData: () => ({}),
|
|
||||||
afterLoad: data => {},
|
|
||||||
beforeSave: data => {
|
beforeSave: data => {
|
||||||
|
data[prefix + 'data'] = data[prefix + 'grapesJSData'];
|
||||||
clearBeforeSave(data);
|
clearBeforeSave(data);
|
||||||
},
|
},
|
||||||
afterTypeChange: mutState => {},
|
afterTypeChange: mutState => {},
|
||||||
|
@ -217,7 +212,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
typeName: t('CKEditor 4'),
|
typeName: t('CKEditor 4'),
|
||||||
getTypeForm: (owner, isEdit) => null,
|
getTypeForm: (owner, isEdit) => null,
|
||||||
getHTMLEditor: owner =>
|
getHTMLEditor: owner =>
|
||||||
<AlignedRow label={t('Template content')}>
|
<AlignedRow label={t('Template content (HTML)')}>
|
||||||
<CKEditorHost
|
<CKEditorHost
|
||||||
ref={node => owner.editorNode = node}
|
ref={node => owner.editorNode = node}
|
||||||
entity={owner.props.entity}
|
entity={owner.props.entity}
|
||||||
|
@ -243,7 +238,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
|
||||||
templateTypes.ckeditor5 = {
|
templateTypes.ckeditor5 = {
|
||||||
typeName: t('CKEditor 5'),
|
typeName: t('CKEditor 5'),
|
||||||
getTypeForm: (owner, isEdit) => null,
|
getTypeForm: (owner, isEdit) => null,
|
||||||
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content')}/>,
|
getHTMLEditor: owner => <CKEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
|
||||||
exportHTMLEditorData: async owner => {},
|
exportHTMLEditorData: async owner => {},
|
||||||
initData: () => ({}),
|
initData: () => ({}),
|
||||||
afterLoad: data => {},
|
afterLoad: data => {},
|
||||||
|
|
|
@ -16,6 +16,7 @@ module.exports = {
|
||||||
"root": ['babel-polyfill', './src/root.js'],
|
"root": ['babel-polyfill', './src/root.js'],
|
||||||
"mosaico-root": ['babel-polyfill', './src/lib/sandboxed-mosaico-root.js'],
|
"mosaico-root": ['babel-polyfill', './src/lib/sandboxed-mosaico-root.js'],
|
||||||
"ckeditor-root": ['babel-polyfill', './src/lib/sandboxed-ckeditor-root.js'],
|
"ckeditor-root": ['babel-polyfill', './src/lib/sandboxed-ckeditor-root.js'],
|
||||||
|
"grapesjs-root": ['babel-polyfill', './src/lib/sandboxed-grapesjs-root.js'],
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
library: 'MailtrainReactBody',
|
library: 'MailtrainReactBody',
|
||||||
|
@ -51,21 +52,23 @@ module.exports = {
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: [
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'style-loader',
|
loader: 'style-loader'
|
||||||
options: {
|
|
||||||
singleton: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: 'css-loader'
|
loader: 'css-loader'
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /ckeditor5-[^/]+\/theme\/[\w-/]+\.css$/,
|
||||||
|
use: [
|
||||||
{
|
{
|
||||||
loader: 'postcss-loader',
|
loader: 'postcss-loader',
|
||||||
options: styles.getPostCssConfig( {
|
options: styles.getPostCssConfig( {
|
||||||
themeImporter: {
|
themeImporter: {
|
||||||
themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
|
themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
|
||||||
},
|
},
|
||||||
minify: false
|
minify: true
|
||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -81,10 +84,6 @@ module.exports = {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: [ 'raw-loader' ]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
exclude: path.join(__dirname, 'node_modules'),
|
exclude: path.join(__dirname, 'node_modules'),
|
||||||
|
@ -97,11 +96,21 @@ module.exports = {
|
||||||
localIdentName: '[path][name]__[local]--[hash:base64:5]'
|
localIdentName: '[path][name]__[local]--[hash:base64:5]'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'sass-loader' ]
|
'sass-loader'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(otf|woff2|woff|ttf|eot)$/,
|
test: /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg|ckeditor-insert-image\.svg$/,
|
||||||
use: [ 'raw-loader' ]
|
use: [
|
||||||
|
'raw-loader'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(svg|otf|woff2|woff|ttf|eot)$/,
|
||||||
|
exclude: /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg|ckeditor-insert-image\.svg$/,
|
||||||
|
use: [
|
||||||
|
'url-loader'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -116,6 +125,6 @@ module.exports = {
|
||||||
],
|
],
|
||||||
watchOptions: {
|
watchOptions: {
|
||||||
ignored: 'node_modules/',
|
ignored: 'node_modules/',
|
||||||
poll: 1000
|
poll: 2000
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -35,10 +35,8 @@ users.registerRestrictedAccessTokenMethod('ckeditor', async ({entityTypeId, enti
|
||||||
|
|
||||||
function getRouter(appType) {
|
function getRouter(appType) {
|
||||||
const router = routerFactory.create();
|
const router = routerFactory.create();
|
||||||
|
|
||||||
if (appType === AppType.SANDBOXED) {
|
|
||||||
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file');
|
|
||||||
|
|
||||||
|
if (appType === AppType.SANDBOXED) {
|
||||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
|
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
|
||||||
|
|
||||||
|
|
59
routes/sandboxed-grapesjs.js
Normal file
59
routes/sandboxed-grapesjs.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const routerFactory = require('../lib/router-async');
|
||||||
|
const passport = require('../lib/passport');
|
||||||
|
const clientHelpers = require('../lib/client-helpers');
|
||||||
|
const users = require('../models/users');
|
||||||
|
|
||||||
|
const files = require('../models/files');
|
||||||
|
const fileHelpers = require('../lib/file-helpers');
|
||||||
|
|
||||||
|
const templates = require('../models/templates');
|
||||||
|
|
||||||
|
const contextHelpers = require('../lib/context-helpers');
|
||||||
|
|
||||||
|
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
|
||||||
|
const { AppType } = require('../shared/app');
|
||||||
|
|
||||||
|
|
||||||
|
users.registerRestrictedAccessTokenMethod('grapesjs', async ({entityTypeId, entityId}) => {
|
||||||
|
if (entityTypeId === 'template') {
|
||||||
|
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
|
||||||
|
|
||||||
|
if (tmpl.type === 'grapesjs') {
|
||||||
|
return {
|
||||||
|
permissions: {
|
||||||
|
'template': {
|
||||||
|
[entityId]: new Set(['manageFiles', 'view'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function getRouter(appType) {
|
||||||
|
const router = routerFactory.create();
|
||||||
|
|
||||||
|
if (appType === AppType.SANDBOXED) {
|
||||||
|
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||||
|
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
|
||||||
|
|
||||||
|
res.render('grapesjs/root', {
|
||||||
|
layout: 'grapesjs/layout',
|
||||||
|
reactCsrfToken: req.csrfToken(),
|
||||||
|
mailtrainConfig: JSON.stringify(mailtrainConfig),
|
||||||
|
scriptFiles: [
|
||||||
|
getSandboxUrl('mailtrain/common.js'),
|
||||||
|
getSandboxUrl('mailtrain/grapesjs-root.js')
|
||||||
|
],
|
||||||
|
publicPath: getSandboxUrl()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.getRouter = getRouter;
|
|
@ -10,9 +10,7 @@
|
||||||
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||||
<link rel="icon" href="{{publicPath}}static/favicon.ico">
|
<link rel="icon" href="{{publicPath}}static/favicon.ico">
|
||||||
|
|
||||||
<title>Mailtrain
|
<title>Mailtrain</title>
|
||||||
{{#if title}} | {{title}}{{/if}}
|
|
||||||
</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{publicPath}}static/bootstrap/themes/united.min.css">
|
<link rel="stylesheet" href="{{publicPath}}static/bootstrap/themes/united.min.css">
|
||||||
<script src="{{publicPath}}static/jquery-2.2.1.min.js"></script>
|
<script src="{{publicPath}}static/jquery-2.2.1.min.js"></script>
|
||||||
|
|
33
views/grapesjs/layout.hbs
Normal file
33
views/grapesjs/layout.hbs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
|
||||||
|
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||||
|
<link rel="icon" href="{{publicPath}}static/favicon.ico">
|
||||||
|
|
||||||
|
<title>Mailtrain</title>
|
||||||
|
|
||||||
|
<script src="{{publicPath}}static/jquery-2.2.1.min.js"></script>
|
||||||
|
|
||||||
|
{{#if mailtrainConfig}}
|
||||||
|
<script>
|
||||||
|
{{#if reactCsrfToken}}window.csfrToken = '{{reactCsrfToken}}';{{/if}}
|
||||||
|
window.mailtrainConfig = {{{mailtrainConfig}}};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{#each scriptFiles}}
|
||||||
|
<script src="{{this}}"></script>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="grapesjs-body">
|
||||||
|
{{{body}}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
6
views/grapesjs/root.hbs
Normal file
6
views/grapesjs/root.hbs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
MailtrainReactBody.default();
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,11 +1,14 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
||||||
<meta name="viewport" content="width=1024, initial-scale=1">
|
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
|
||||||
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
||||||
<link rel="icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="{{publicPath}}static/favicon.ico">
|
||||||
|
|
||||||
<title>Mailtrain</title>
|
<title>Mailtrain</title>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue