From e2093e22fe2279adc3ef07d21e05807f2d9a6ea1 Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Tue, 6 Nov 2018 13:30:50 +0100 Subject: [PATCH] Some fixes and optimizations in sandboxes. Start of a sandbox for GrapeJS --- app-builder.js | 2 + client/package-lock.json | 10 ++ client/package.json | 1 + client/src/lib/grapesjs.js | 48 ------ client/src/lib/grapesjs.scss | 5 - client/src/lib/public-path.js | 2 +- client/src/lib/sandboxed-ckeditor-root.js | 133 ++++++++++++++++- client/src/lib/sandboxed-ckeditor-shared.js | 3 + client/src/lib/sandboxed-ckeditor.js | 80 +--------- client/src/lib/sandboxed-grapesjs-root.js | 157 ++++++++++++++++++++ client/src/lib/sandboxed-grapesjs.js | 73 +++++++++ client/src/lib/sandboxed-grapesjs.scss | 86 +++++++++++ client/src/lib/sandboxed-mosaico-root.js | 120 ++++++++++++++- client/src/lib/sandboxed-mosaico.js | 111 +------------- client/src/templates/helpers.js | 49 +++--- client/webpack.config.js | 37 +++-- routes/sandboxed-ckeditor.js | 4 +- routes/sandboxed-grapesjs.js | 59 ++++++++ views/ckeditor/layout.hbs | 4 +- views/grapesjs/layout.hbs | 33 ++++ views/grapesjs/root.hbs | 6 + views/mosaico/layout.hbs | 13 +- 22 files changed, 742 insertions(+), 294 deletions(-) delete mode 100644 client/src/lib/grapesjs.js delete mode 100644 client/src/lib/grapesjs.scss create mode 100644 client/src/lib/sandboxed-ckeditor-shared.js create mode 100644 client/src/lib/sandboxed-grapesjs-root.js create mode 100644 client/src/lib/sandboxed-grapesjs.js create mode 100644 client/src/lib/sandboxed-grapesjs.scss create mode 100644 routes/sandboxed-grapesjs.js create mode 100644 views/grapesjs/layout.hbs create mode 100644 views/grapesjs/root.hbs diff --git a/app-builder.js b/app-builder.js index 9dd30ac0..3be78a84 100644 --- a/app-builder.js +++ b/app-builder.js @@ -24,6 +24,7 @@ const reports = require('./routes/reports'); const subscription = require('./routes/subscription'); const sandboxedMosaico = require('./routes/sandboxed-mosaico'); const sandboxedCKEditor = require('./routes/sandboxed-ckeditor'); +const sandboxedGrapesJS = require('./routes/sandboxed-grapesjs'); const files = require('./routes/files'); const links = require('./routes/links'); const archive = require('./routes/archive'); @@ -224,6 +225,7 @@ function createApp(appType) { useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType)); useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType)); + useWith404Fallback('/grapesjs', sandboxedGrapesJS.getRouter(appType)); if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) { if (config.reports && config.reports.enabled === true) { diff --git a/client/package-lock.json b/client/package-lock.json index 9f883842..7e243d8b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -3837,6 +3837,16 @@ "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", diff --git a/client/package.json b/client/package.json index b3266a92..55d69ffc 100644 --- a/client/package.json +++ b/client/package.json @@ -71,6 +71,7 @@ "babel-preset-react": "^6.24.1", "babel-preset-stage-1": "^6.24.1", "css-loader": "^0.28.4", + "file-loader": "^2.0.0", "i18next-conv": "^3.0.3", "node-sass": "^4.5.3", "postcss-loader": "^3.0.0", diff --git a/client/src/lib/grapesjs.js b/client/src/lib/grapesjs.js deleted file mode 100644 index b9d7f1ae..00000000 --- a/client/src/lib/grapesjs.js +++ /dev/null @@ -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: - ' \n' + - ' \n' + - ' \n' + - ' My Company\n' + - ' \n' + - ' \n' + - ' ', - plugins: ['gjs-mjml'], - pluginsOpts: { - 'gjs-mjml': {} - } - }); - } - - render() { - return ( -
-
this.canvasNode = node}/> -
- ); - } -} \ No newline at end of file diff --git a/client/src/lib/grapesjs.scss b/client/src/lib/grapesjs.scss deleted file mode 100644 index 1d22bb61..00000000 --- a/client/src/lib/grapesjs.scss +++ /dev/null @@ -1,5 +0,0 @@ -.gjs-cv-canvas { - top: 0; - width: 100%; - height: 100%; -} \ No newline at end of file diff --git a/client/src/lib/public-path.js b/client/src/lib/public-path.js index b2dacaf1..f408e853 100644 --- a/client/src/lib/public-path.js +++ b/client/src/lib/public-path.js @@ -2,4 +2,4 @@ import {getUrl} from "./urls"; -__webpack_public_path__ = getUrl(); +__webpack_public_path__ = getUrl('mailtrain/'); diff --git a/client/src/lib/sandboxed-ckeditor-root.js b/client/src/lib/sandboxed-ckeditor-root.js index fb84d713..cb78b30f 100644 --- a/client/src/lib/sandboxed-ckeditor-root.js +++ b/client/src/lib/sandboxed-ckeditor-root.js @@ -2,12 +2,133 @@ import './public-path'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import {I18nextProvider,} from 'react-i18next'; -import i18n from './i18n'; -import {CKEditorSandbox} from './sandboxed-ckeditor'; -import {UntrustedContentRoot, parentRPC} from './untrusted'; +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 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 ( +
+ this.node = node} + content={this.state.html} + events={{ + change: evt => this.setState({html: evt.editor.getData()}), + }} + config={config} + /> +
+ ); + } +} export default function() { parentRPC.init(); diff --git a/client/src/lib/sandboxed-ckeditor-shared.js b/client/src/lib/sandboxed-ckeditor-shared.js new file mode 100644 index 00000000..8754615f --- /dev/null +++ b/client/src/lib/sandboxed-ckeditor-shared.js @@ -0,0 +1,3 @@ +'use strict'; + +export const initialHeight = 600; diff --git a/client/src/lib/sandboxed-ckeditor.js b/client/src/lib/sandboxed-ckeditor.js index 80113b12..8f684351 100644 --- a/client/src/lib/sandboxed-ckeditor.js +++ b/client/src/lib/sandboxed-ckeditor.js @@ -2,23 +2,16 @@ import React, {Component} from 'react'; import {translate} from 'react-i18next'; -import PropTypes from "prop-types"; -import styles from "./sandboxed-ckeditor.scss"; +import PropTypes + from "prop-types"; +import styles + from "./sandboxed-ckeditor.scss"; -import {UntrustedContentHost, parentRPC} from './untrusted'; +import {UntrustedContentHost} from './untrusted'; import {Icon} from "./bootstrap-components"; -import { - getPublicUrl, - getSandboxUrl, - getTrustedUrl -} from "./urls"; -import { - base, - unbase -} from "../../../shared/templates"; -import CKEditor from "react-ckeditor-component"; +import {getTrustedUrl} from "./urls"; -const initialHeight = 600; +import { initialHeight } from "./sandboxed-ckeditor-shared"; const navbarHeight = 34; // Sync this with navbarheight in sandboxed-ckeditor.scss @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 ( -
- this.node = node} - content={this.state.html} - events={{ - change: evt => this.setState({html: evt.editor.getData()}), - }} - config={config} - /> -
- ); - } -} diff --git a/client/src/lib/sandboxed-grapesjs-root.js b/client/src/lib/sandboxed-grapesjs-root.js new file mode 100644 index 00000000..696ea705 --- /dev/null +++ b/client/src/lib/sandboxed-grapesjs-root.js @@ -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); + + /* + ' \n' + + ' \n' + + ' \n' + + ' My Company\n' + + ' \n' + + ' \n' + + ' ', + */ + + 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 ( +
+
this.canvasNode = node}/> +
+ ); + } +} + + + +export default function() { + parentRPC.init(); + + ReactDOM.render( + + } /> + , + document.getElementById('root') + ); +}; + + diff --git a/client/src/lib/sandboxed-grapesjs.js b/client/src/lib/sandboxed-grapesjs.js new file mode 100644 index 00000000..1cef29bf --- /dev/null +++ b/client/src/lib/sandboxed-grapesjs.js @@ -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 ( +
+
+ {this.state.fullscreen && } +
{this.props.title}
+ +
+ this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="grapesjs/editor" tokenMethod="grapesjs" tokenParams={tokenData}/> +
+ ); + } +} + +GrapesJSHost.prototype.exportState = async function() { + return await this.getWrappedInstance().exportState(); +}; diff --git a/client/src/lib/sandboxed-grapesjs.scss b/client/src/lib/sandboxed-grapesjs.scss new file mode 100644 index 00000000..cdffac64 --- /dev/null +++ b/client/src/lib/sandboxed-grapesjs.scss @@ -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; +} + diff --git a/client/src/lib/sandboxed-mosaico-root.js b/client/src/lib/sandboxed-mosaico-root.js index 76753c13..17366f3b 100644 --- a/client/src/lib/sandboxed-mosaico-root.js +++ b/client/src/lib/sandboxed-mosaico-root.js @@ -2,12 +2,120 @@ import './public-path'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import {I18nextProvider,} from 'react-i18next'; -import i18n from './i18n'; -import {MosaicoSandbox} from './sandboxed-mosaico'; -import {UntrustedContentRoot, parentRPC} from './untrusted'; +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"; + + +@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
; + } +} + + export default function() { parentRPC.init(); diff --git a/client/src/lib/sandboxed-mosaico.js b/client/src/lib/sandboxed-mosaico.js index c11da205..98ea5897 100644 --- a/client/src/lib/sandboxed-mosaico.js +++ b/client/src/lib/sandboxed-mosaico.js @@ -2,24 +2,18 @@ import React, {Component} from 'react'; import {translate} from 'react-i18next'; -import PropTypes from "prop-types"; -import styles from "./sandboxed-mosaico.scss"; +import PropTypes + from "prop-types"; +import styles + from "./sandboxed-mosaico.scss"; -import {UntrustedContentHost, parentRPC} from './untrusted'; +import {UntrustedContentHost} from './untrusted'; import {Icon} from "./bootstrap-components"; -import { - getPublicUrl, - getSandboxUrl, - getTrustedUrl -} from "./urls"; -import { - base, - unbase -} from "../../../shared/templates"; +import {getTrustedUrl} from "./urls"; @translate(null, { withRef: true }) -export class MosaicoEditorHost extends Component { +export class MosaicoHost extends Component { constructor(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(); }; - - -@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
; - } -} - diff --git a/client/src/templates/helpers.js b/client/src/templates/helpers.js index 4a6963ed..6ac096cf 100644 --- a/client/src/templates/helpers.js +++ b/client/src/templates/helpers.js @@ -12,9 +12,9 @@ import { import 'brace/mode/text'; import 'brace/mode/html'; -import { MosaicoEditorHost } from "../lib/sandboxed-mosaico"; +import { MosaicoHost } from "../lib/sandboxed-mosaico"; 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 {getSandboxUrl} from "../lib/urls"; @@ -70,7 +70,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM , getHTMLEditor: owner => - owner.editorNode = node} entity={owner.props.entity} initialModel={owner.getFormValue(prefix + 'mosaicoData').model} @@ -132,7 +132,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM }, getHTMLEditor: owner => - owner.editorNode = node} entity={owner.props.entity} initialModel={owner.getFormValue(prefix + 'mosaicoData').model} @@ -175,38 +175,33 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM 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 = { - typeName: t('GrapeJS (fake)'), + typeName: t('GrapeJS'), getTypeForm: (owner, isEdit) => null, getHTMLEditor: owner => - owner.editorNode = node} entity={owner.props.entity} - initialHtml={owner.getFormValue(prefix + 'html')} - entityTypeId={entityTypeId}/> + initialModel={owner.getFormValue(prefix + 'grapesJSData')} + entityTypeId={entityTypeId} + title={t('GrapesJS Template Designer')} + onFullscreenAsync={::owner.setElementInFullscreen} + /> , exportHTMLEditorData: async owner => { - const {html} = await owner.editorNode.exportState(); + const {html, model} = await owner.editorNode.exportState(); 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 => { + data[prefix + 'data'] = data[prefix + 'grapesJSData']; clearBeforeSave(data); }, afterTypeChange: mutState => {}, @@ -217,7 +212,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM typeName: t('CKEditor 4'), getTypeForm: (owner, isEdit) => null, getHTMLEditor: owner => - + owner.editorNode = node} entity={owner.props.entity} @@ -243,7 +238,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM templateTypes.ckeditor5 = { typeName: t('CKEditor 5'), getTypeForm: (owner, isEdit) => null, - getHTMLEditor: owner => , + getHTMLEditor: owner => , exportHTMLEditorData: async owner => {}, initData: () => ({}), afterLoad: data => {}, diff --git a/client/webpack.config.js b/client/webpack.config.js index c5600fa7..06d2e303 100644 --- a/client/webpack.config.js +++ b/client/webpack.config.js @@ -16,6 +16,7 @@ module.exports = { "root": ['babel-polyfill', './src/root.js'], "mosaico-root": ['babel-polyfill', './src/lib/sandboxed-mosaico-root.js'], "ckeditor-root": ['babel-polyfill', './src/lib/sandboxed-ckeditor-root.js'], + "grapesjs-root": ['babel-polyfill', './src/lib/sandboxed-grapesjs-root.js'], }, output: { library: 'MailtrainReactBody', @@ -51,21 +52,23 @@ module.exports = { test: /\.css$/, use: [ { - loader: 'style-loader', - options: { - singleton: true - } + loader: 'style-loader' }, { loader: 'css-loader' - }, + } + ] + }, + { + test: /ckeditor5-[^/]+\/theme\/[\w-/]+\.css$/, + use: [ { loader: 'postcss-loader', options: styles.getPostCssConfig( { themeImporter: { themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' ) }, - minify: false + minify: true } ) } ] @@ -81,10 +84,6 @@ module.exports = { } ] }, - { - test: /\.svg$/, - use: [ 'raw-loader' ] - }, { test: /\.scss$/, exclude: path.join(__dirname, 'node_modules'), @@ -97,11 +96,21 @@ module.exports = { localIdentName: '[path][name]__[local]--[hash:base64:5]' } }, - 'sass-loader' ] + 'sass-loader' + ] }, { - test: /\.(otf|woff2|woff|ttf|eot)$/, - use: [ 'raw-loader' ] + test: /ckeditor5-[^/]+\/theme\/icons\/[^/]+\.svg|ckeditor-insert-image\.svg$/, + 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: { ignored: 'node_modules/', - poll: 1000 + poll: 2000 } }; diff --git a/routes/sandboxed-ckeditor.js b/routes/sandboxed-ckeditor.js index 375f14d5..8701832e 100644 --- a/routes/sandboxed-ckeditor.js +++ b/routes/sandboxed-ckeditor.js @@ -35,10 +35,8 @@ users.registerRestrictedAccessTokenMethod('ckeditor', async ({entityTypeId, enti function getRouter(appType) { 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) => { const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType); diff --git a/routes/sandboxed-grapesjs.js b/routes/sandboxed-grapesjs.js new file mode 100644 index 00000000..f1a7d95a --- /dev/null +++ b/routes/sandboxed-grapesjs.js @@ -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; diff --git a/views/ckeditor/layout.hbs b/views/ckeditor/layout.hbs index e64d2c6a..f044a18b 100644 --- a/views/ckeditor/layout.hbs +++ b/views/ckeditor/layout.hbs @@ -10,9 +10,7 @@ - Mailtrain - {{#if title}} | {{title}}{{/if}} - + Mailtrain diff --git a/views/grapesjs/layout.hbs b/views/grapesjs/layout.hbs new file mode 100644 index 00000000..f5da012a --- /dev/null +++ b/views/grapesjs/layout.hbs @@ -0,0 +1,33 @@ + + + + + + + + + + + + + Mailtrain + + + + {{#if mailtrainConfig}} + + + {{#each scriptFiles}} + + {{/each}} + {{/if}} + + + +{{{body}}} + + + diff --git a/views/grapesjs/root.hbs b/views/grapesjs/root.hbs new file mode 100644 index 00000000..53ae02fe --- /dev/null +++ b/views/grapesjs/root.hbs @@ -0,0 +1,6 @@ +
+ \ No newline at end of file diff --git a/views/mosaico/layout.hbs b/views/mosaico/layout.hbs index 5a615068..9bf10ded 100644 --- a/views/mosaico/layout.hbs +++ b/views/mosaico/layout.hbs @@ -1,11 +1,14 @@ - - - - + + + + + + + - + Mailtrain