From c7d7b1fe0ceb7801524f2db62fb43f187073b31f Mon Sep 17 00:00:00 2001 From: Tomas Bures Date: Sun, 11 Nov 2018 01:51:10 +0100 Subject: [PATCH] GrapesJS support more or less finished. --- client/package-lock.json | 18 + client/package.json | 1 + client/src/lib/sandboxed-grapesjs-root.js | 517 ++++++++++++++++++-- client/src/lib/sandboxed-grapesjs-shared.js | 6 + client/src/lib/sandboxed-grapesjs.js | 5 +- client/src/templates/helpers.js | 24 +- 6 files changed, 535 insertions(+), 36 deletions(-) create mode 100644 client/src/lib/sandboxed-grapesjs-shared.js diff --git a/client/package-lock.json b/client/package-lock.json index 0f6ab9a2..580edded 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2180,6 +2180,11 @@ "safe-buffer": "5.1.1" } }, + "ckeditor": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/ckeditor/-/ckeditor-4.11.1.tgz", + "integrity": "sha512-UhHe02cc/wWJquDQZysEgh0ohLMEMU56zDx+s8prDdjylY/aBDY2xdIiIpbgCBTXdjhrEPIAPyiDS9g3RxYXig==" + }, "ckeditor5": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/ckeditor5/-/ckeditor5-11.1.1.tgz", @@ -4909,6 +4914,19 @@ } } }, + "grapesjs-plugin-ckeditor": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/grapesjs-plugin-ckeditor/-/grapesjs-plugin-ckeditor-0.0.9.tgz", + "integrity": "sha512-QXyAcSwgi09pzigGVS/NsHag5Skuw4zTkVGmEiBN/Qi8KU12/cQBG/OjAcjAB3/ZpToyPoglI33Ydjgj2nJuxQ==" + }, + "grapesjs-preset-newsletter": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/grapesjs-preset-newsletter/-/grapesjs-preset-newsletter-0.2.20.tgz", + "integrity": "sha512-rffUeuznf9Saig+kIUddmGfhWwbLjxdaqAYf6Hoge4b0sfT8knOS4mQXJBdRsSROfzuRhFe6ybRHm4yC32lHxA==", + "requires": { + "juice": "4.3.2" + } + }, "har-validator": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", diff --git a/client/package.json b/client/package.json index 38d56f04..43940f59 100644 --- a/client/package.json +++ b/client/package.json @@ -39,6 +39,7 @@ "datatables.net-bs": "^1.10.15", "grapesjs": "^0.14.40", "grapesjs-mjml": "0.0.27", + "grapesjs-preset-newsletter": "^0.2.20", "i18next": "^8.4.3", "i18next-xhr-backend": "^1.4.2", "immutable": "^3.8.1", diff --git a/client/src/lib/sandboxed-grapesjs-root.js b/client/src/lib/sandboxed-grapesjs-root.js index b92c6d08..2a4451e5 100644 --- a/client/src/lib/sandboxed-grapesjs-root.js +++ b/client/src/lib/sandboxed-grapesjs-root.js @@ -31,16 +31,26 @@ import mjml2html from "mjml4-in-browser"; import 'grapesjs/dist/css/grapes.min.css'; import grapesjs from 'grapesjs'; -import "grapesjs-mjml"; + +import 'grapesjs-mjml'; + +import 'grapesjs-preset-newsletter'; +import 'grapesjs-preset-newsletter/dist/grapesjs-preset-newsletter.css'; import "./sandboxed-grapesjs.scss"; import axios from './axios'; +import {GrapesJSSourceType} from "./sandboxed-grapesjs-shared"; -grapesjs.plugins.add('mailtrain', (editor, opts = {}) => { - const panelManager = editor.Panels; - panelManager.removeButton('options','fullscreen'); - panelManager.removeButton('options','export-template'); + +grapesjs.plugins.add('mailtrain-remove-buttons', (editor, opts = {}) => { + // This needs to be done in on-load and after gjs plugin because grapesjs-preset-newsletter tries to set titles to all buttons (including those we remove) + // see https://github.com/artf/grapesjs-preset-newsletter/blob/e0a91636973a5a1481e9d7929e57a8869b1db72e/src/index.js#L248 + editor.on('load', () => { + const panelManager = editor.Panels; + panelManager.removeButton('options','fullscreen'); + panelManager.removeButton('options','export-template'); + }); }); @@ -60,16 +70,19 @@ export class GrapesJSSandbox extends Component { entityTypeId: PropTypes.string, entityId: PropTypes.number, initialSource: PropTypes.string, - initialStyle: PropTypes.string + initialStyle: PropTypes.string, + sourceType: PropTypes.string } async exportState(method, params) { + const props = this.props; + const editor = this.editor; // If exportState comes during text editing (via RichTextEditor), we need to cancel the editing, so that the // text being edited is stored in the model const sel = editor.getSelected(); - if (sel && sel.view && sel.disableEditing) { + if (sel && sel.view && sel.view.disableEditing) { sel.view.disableEditing(); } @@ -82,11 +95,21 @@ export class GrapesJSSandbox extends Component { let html; - const preMjml = ''; - const postMjml = ''; - const mjml = preMjml + source + postMjml; + if (props.sourceType === GrapesJSSourceType.MJML) { + const preMjml = ''; + const postMjml = ''; + const mjml = preMjml + source + postMjml; + + const mjmlRes = mjml2html(mjml); + html = mjmlRes.html; + + } else if (props.sourceType === GrapesJSSourceType.HTML) { + const commandManager = editor.Commands; + + const cmdGetCode = commandManager.get('gjs-get-inlined-html'); + html = cmdGetCode.run(editor); + } - const mjmlRes = mjml2html(mjml); return { html, @@ -124,19 +147,7 @@ export class GrapesJSSandbox extends Component { const sandboxUrlBase = getSandboxUrl(); const publicUrlBase = getPublicUrl(); - const source = props.initialSource ? - base(props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase) : - ' \n' + - ' \n' + - ' \n' + - ' My Company\n' + - ' \n' + - ' \n' + - ' '; - - const css = props.initialStyle && base(props.initialStyle, trustedUrlBase, sandboxUrlBase, publicUrlBase); - - this.editor = grapesjs.init({ + const config = { noticeOnUnload: false, container: this.canvasNode, height: '100%', @@ -157,19 +168,458 @@ export class GrapesJSSandbox extends Component { clearProperties: true, }, fromElement: false, - components: source, - style: css, + components: '', + style: '', plugins: [ - 'mailtrain', - 'gjs-mjml' ], pluginsOpts: { - 'gjs-mjml': { - preMjml: '', - postMjml: '' - } } - }); + }; + + let defaultSource, defaultStyle; + + if (props.sourceType === GrapesJSSourceType.MJML) { + defaultSource = + '\n' + + ' \n' + + ' \n' + + ' Lorem Ipsum...\n' + + ' \n' + + ' \n' + + ''; + + defaultStyle = ''; + + config.plugins.push('gjs-mjml'); + config.pluginsOpts['gjs-mjml'] = { + preMjml: '', + postMjml: '' + }; + + } else if (props.sourceType === GrapesJSSourceType.HTML) { + defaultSource = + '\n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' View in browser\n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' GrapesJS.\n' + + ' \n' + + '
GrapesJS Newsletter Builder\n' + + '
\n' + + '
\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' Big image here\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + '

Build your newsletters faster than ever\n' + + '
\n' + + '

\n' + + '

Import, build, test and export responsive newsletter templates faster than ever using the GrapesJS Newsletter Builder.\n' + + '

\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + '
\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' Image1\n' + + ' \n' + + '

Built-in Blocks\n' + + '

\n' + + '

Drag and drop built-in blocks from the right panel and style them in a matter of seconds\n' + + '

\n' + + '
\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' Image2\n' + + ' \n' + + '

Toggle images\n' + + '

\n' + + '

Build a good looking newsletter even without images enabled by the email clients\n' + + '

\n' + + '
\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' Image1\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + '

Test it\n' + + '

\n' + + '

You can send email tests directly from the editor and check how are looking on your email clients\n' + + '

\n' + + '
\n' + + '
\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + ' Image2\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + '

Responsive\n' + + '

\n' + + '

Using the device manager you\'ll always send a fully responsive contents\n' + + '

\n' + + '
\n' + + '
\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
\n' + + '
'; + + defaultStyle = + '.link {\n' + + ' color: rgb(217, 131, 166);\n' + + ' }\n' + + ' .row{\n' + + ' vertical-align:top;\n' + + ' }\n' + + ' .main-body{\n' + + ' min-height:150px;\n' + + ' padding: 5px;\n' + + ' width:100%;\n' + + ' height:100%;\n' + + ' background-color:rgb(234, 236, 237);\n' + + ' }\n' + + ' .c926{\n' + + ' color:rgb(158, 83, 129);\n' + + ' width:100%;\n' + + ' font-size:50px;\n' + + ' }\n' + + ' .cell.c849{\n' + + ' width:11%;\n' + + ' }\n' + + ' .c1144{\n' + + ' padding: 10px;\n' + + ' font-size:17px;\n' + + ' font-weight: 300;\n' + + ' }\n' + + ' .card{\n' + + ' min-height:150px;\n' + + ' padding: 5px;\n' + + ' margin-bottom:20px;\n' + + ' height:0px;\n' + + ' }\n' + + ' .card-cell{\n' + + ' background-color:rgb(255, 255, 255);\n' + + ' overflow:hidden;\n' + + ' border-radius: 3px;\n' + + ' padding: 0;\n' + + ' text-align:center;\n' + + ' }\n' + + ' .card.sector{\n' + + ' background-color:rgb(255, 255, 255);\n' + + ' border-radius: 3px;\n' + + ' border-collapse:separate;\n' + + ' }\n' + + ' .c1271{\n' + + ' width:100%;\n' + + ' margin: 0 0 15px 0;\n' + + ' font-size:50px;\n' + + ' color:rgb(120, 197, 214);\n' + + ' line-height:250px;\n' + + ' text-align:center;\n' + + ' }\n' + + ' .table100{\n' + + ' width:100%;\n' + + ' }\n' + + ' .c1357{\n' + + ' min-height:150px;\n' + + ' padding: 5px;\n' + + ' margin: auto;\n' + + ' height:0px;\n' + + ' }\n' + + ' .darkerfont{\n' + + ' color:rgb(65, 69, 72);\n' + + ' }\n' + + ' .button{\n' + + ' font-size:12px;\n' + + ' padding: 10px 20px;\n' + + ' background-color:rgb(217, 131, 166);\n' + + ' color:rgb(255, 255, 255);\n' + + ' text-align:center;\n' + + ' border-radius: 3px;\n' + + ' font-weight:300;\n' + + ' }\n' + + ' .table100.c1437{\n' + + ' text-align:left;\n' + + ' }\n' + + ' .cell.cell-bottom{\n' + + ' text-align:center;\n' + + ' height:51px;\n' + + ' }\n' + + ' .card-title{\n' + + ' font-size:25px;\n' + + ' font-weight:300;\n' + + ' color:rgb(68, 68, 68);\n' + + ' }\n' + + ' .card-content{\n' + + ' font-size:13px;\n' + + ' line-height:20px;\n' + + ' color:rgb(111, 119, 125);\n' + + ' padding: 10px 20px 0 20px;\n' + + ' vertical-align:top;\n' + + ' }\n' + + ' .container{\n' + + ' font-family: Helvetica, serif;\n' + + ' min-height:150px;\n' + + ' padding: 5px;\n' + + ' margin:auto;\n' + + ' height:0px;\n' + + ' width:90%;\n' + + ' max-width:550px;\n' + + ' }\n' + + ' .cell.c856{\n' + + ' vertical-align:middle;\n' + + ' }\n' + + ' .container-cell{\n' + + ' vertical-align:top;\n' + + ' font-size:medium;\n' + + ' padding-bottom:50px;\n' + + ' }\n' + + ' .c1790{\n' + + ' min-height:150px;\n' + + ' padding: 5px;\n' + + ' margin:auto;\n' + + ' height:0px;\n' + + ' }\n' + + ' .table100.c1790{\n' + + ' min-height:30px;\n' + + ' border-collapse:separate;\n' + + ' margin: 0 0 10px 0;\n' + + ' }\n' + + ' .browser-link{\n' + + ' font-size:12px;\n' + + ' }\n' + + ' .top-cell{\n' + + ' text-align:right;\n' + + ' color:rgb(152, 156, 165);\n' + + ' }\n' + + ' .table100.c1357{\n' + + ' margin: 0;\n' + + ' border-collapse:collapse;\n' + + ' }\n' + + ' .c1769{\n' + + ' width:30%;\n' + + ' }\n' + + ' .c1776{\n' + + ' width:70%;\n' + + ' }\n' + + ' .c1766{\n' + + ' margin: 0 auto 10px 0;\n' + + ' padding: 5px;\n' + + ' width:100%;\n' + + ' min-height:30px;\n' + + ' }\n' + + ' .cell.c1769{\n' + + ' width:11%;\n' + + ' }\n' + + ' .cell.c1776{\n' + + ' vertical-align:middle;\n' + + ' }\n' + + ' .c1542{\n' + + ' margin: 0 auto 10px auto;\n' + + ' padding:5px;\n' + + ' width:100%;\n' + + ' }\n' + + ' .card-footer{\n' + + ' padding: 20px 0;\n' + + ' text-align:center;\n' + + ' }\n' + + ' .c2280{\n' + + ' height:150px;\n' + + ' margin:0 auto 10px auto;\n' + + ' padding:5px 5px 5px 5px;\n' + + ' width:100%;\n' + + ' }\n' + + ' .c2421{\n' + + ' padding:10px;\n' + + ' }\n' + + ' .c2577{\n' + + ' padding:10px;\n' + + ' }\n' + + ' .footer{\n' + + ' margin-top: 50px;\n' + + ' color:rgb(152, 156, 165);\n' + + ' text-align:center;\n' + + ' font-size:11px;\n' + + ' padding: 5px;\n' + + ' }\n' + + ' .quote {\n' + + ' font-style: italic;\n' + + ' }\n' + + ' .list-item{\n' + + ' height:auto;\n' + + ' width:100%;\n' + + ' margin: 0 auto 10px auto;\n' + + ' padding: 5px;\n' + + ' }\n' + + ' .list-item-cell{\n' + + ' background-color:rgb(255, 255, 255);\n' + + ' border-radius: 3px;\n' + + ' overflow: hidden;\n' + + ' padding: 0;\n' + + ' }\n' + + ' .list-cell-left{\n' + + ' width:30%;\n' + + ' padding: 0;\n' + + ' }\n' + + ' .list-cell-right{\n' + + ' width:70%;\n' + + ' color:rgb(111, 119, 125);\n' + + ' font-size:13px;\n' + + ' line-height:20px;\n' + + ' padding: 10px 20px 0px 20px;\n' + + ' }\n' + + ' .list-item-content{\n' + + ' border-collapse: collapse;\n' + + ' margin: 0 auto;\n' + + ' padding: 5px;\n' + + ' height:150px;\n' + + ' width:100%;\n' + + ' }\n' + + ' .list-item-image{\n' + + ' color:rgb(217, 131, 166);\n' + + ' font-size:45px;\n' + + ' width: 100%;\n' + + ' }\n' + + ' .grid-item-image{\n' + + ' line-height:150px;\n' + + ' font-size:50px;\n' + + ' color:rgb(120, 197, 214);\n' + + ' margin-bottom:15px;\n' + + ' width:100%;\n' + + ' }\n' + + ' .grid-item-row {\n' + + ' margin: 0 auto 10px;\n' + + ' padding: 5px 0;\n' + + ' width: 100%;\n' + + ' }\n' + + ' .grid-item-card {\n' + + ' width:100%;\n' + + ' padding: 5px 0;\n' + + ' margin-bottom: 10px;\n' + + ' }\n' + + ' .grid-item-card-cell{\n' + + ' background-color:rgb(255, 255, 255);\n' + + ' overflow: hidden;\n' + + ' border-radius: 3px;\n' + + ' text-align:center;\n' + + ' padding: 0;\n' + + ' }\n' + + ' .grid-item-card-content{\n' + + ' font-size:13px;\n' + + ' color:rgb(111, 119, 125);\n' + + ' padding: 0 10px 20px 10px;\n' + + ' width:100%;\n' + + ' line-height:20px;\n' + + ' }\n' + + ' .grid-item-cell2-l{\n' + + ' vertical-align:top;\n' + + ' padding-right:10px;\n' + + ' width:50%;\n' + + ' }\n' + + ' .grid-item-cell2-r{\n' + + ' vertical-align:top;\n' + + ' padding-left:10px;\n' + + ' width:50%;\n' + + ' }'; + + config.plugins.push('gjs-preset-newsletter'); + } + + config.components = props.initialSource ? base(props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource; + config.style = props.initialStyle ? base(props.initialStyle, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultStyle; + + config.plugins.push('mailtrain-remove-buttons'); + + this.editor = grapesjs.init(config); } render() { @@ -182,7 +632,6 @@ export class GrapesJSSandbox extends Component { } - export default function() { parentRPC.init(); diff --git a/client/src/lib/sandboxed-grapesjs-shared.js b/client/src/lib/sandboxed-grapesjs-shared.js new file mode 100644 index 00000000..339580c4 --- /dev/null +++ b/client/src/lib/sandboxed-grapesjs-shared.js @@ -0,0 +1,6 @@ +'use strict'; + +export const GrapesJSSourceType = { + MJML: 'mjml', + HTML: 'html' +}; diff --git a/client/src/lib/sandboxed-grapesjs.js b/client/src/lib/sandboxed-grapesjs.js index fdca6651..1dabee78 100644 --- a/client/src/lib/sandboxed-grapesjs.js +++ b/client/src/lib/sandboxed-grapesjs.js @@ -10,6 +10,7 @@ import styles import {UntrustedContentHost} from './untrusted'; import {Icon} from "./bootstrap-components"; import {getTrustedUrl} from "./urls"; +import {GrapesJSSourceType} from "./sandboxed-grapesjs-shared"; @translate(null, { withRef: true }) export class GrapesJSHost extends Component { @@ -26,6 +27,7 @@ export class GrapesJSHost extends Component { entity: PropTypes.object, initialSource: PropTypes.string, initialStyle: PropTypes.string, + sourceType: PropTypes.string, title: PropTypes.string, onFullscreenAsync: PropTypes.func } @@ -49,7 +51,8 @@ export class GrapesJSHost extends Component { entityTypeId: this.props.entityTypeId, entityId: this.props.entity.id, initialSource: this.props.initialSource, - initialStyle: this.props.initialStyle + initialStyle: this.props.initialStyle, + sourceType: this.props.sourceType }; const tokenData = { diff --git a/client/src/templates/helpers.js b/client/src/templates/helpers.js index 847028f1..7d71a29c 100644 --- a/client/src/templates/helpers.js +++ b/client/src/templates/helpers.js @@ -26,6 +26,7 @@ import { import {Trans} from "react-i18next"; import styles from "../lib/styles.scss"; +import {GrapesJSSourceType} from "../lib/sandboxed-grapesjs-shared"; export const ResourceType = { TEMPLATE: 'template', @@ -175,9 +176,26 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM validate: state => {} }; + + const grapesJSSourceTypes = [ + {key: GrapesJSSourceType.MJML, label: t('MJML')}, + {key: GrapesJSSourceType.HTML, label: t('HTML')} + ]; + + const grapesJSSourceTypeLabels = {}; + for ({key, label} of grapesJSSourceTypes) { + grapesJSSourceTypeLabels[key] = label; + } + templateTypes.grapesjs = { typeName: t('GrapeJS'), - getTypeForm: (owner, isEdit) => null, + getTypeForm: (owner, isEdit) => { + if (isEdit) { + return {grapesJSSourceTypeLabels[(owner.getFormValue(prefix + 'grapesJSSourceType'))]}; + } else { + return ; + } + }, getHTMLEditor: owner => @@ -199,9 +218,11 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM }); }, initData: () => ({ + [prefix + 'grapesJSSourceType']: GrapesJSSourceType.MJML, [prefix + 'grapesJSData']: {} }), afterLoad: data => { + data[prefix + 'grapesJSSourceType'] = data[prefix + 'data'].sourceType; data[prefix + 'grapesJSData'] = { source: data[prefix + 'data'].source, style: data[prefix + 'data'].style @@ -209,6 +230,7 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM }, beforeSave: data => { data[prefix + 'data'] = { + sourceType: data[prefix + 'grapesJSSourceType'], source: data[prefix + 'grapesJSData'].source, style: data[prefix + 'grapesJSData'].style };