Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a7b2c33

Browse files
committedMay 31, 2017
GrapeJS MJML Integration (Experimental)
Mailtrain-org#215
1 parent ae2b07b commit a7b2c33

File tree

4 files changed

+278
-156
lines changed

4 files changed

+278
-156
lines changed
 

‎config/default.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ templates=[["versafix-1", "Versafix One"]]
151151

152152
[grapejs]
153153
# Installed templates
154-
templates=[["demo", "Demo Template"]]
154+
templates=[
155+
["demo", "HTML Template"],
156+
["aves", "MJML Template"]
157+
]
155158

156159
[reports]
157160
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be

‎routes/grapejs.js

+29-18
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
'use strict';
22

3-
let config = require('config');
4-
let express = require('express');
5-
let router = new express.Router();
6-
let passport = require('../lib/passport');
7-
let fs = require('fs');
8-
let path = require('path');
9-
let editorHelpers = require('../lib/editor-helpers.js')
3+
const config = require('config');
4+
const express = require('express');
5+
const router = new express.Router();
6+
const passport = require('../lib/passport');
7+
const _ = require('../lib/translate')._;
8+
const fs = require('fs');
9+
const path = require('path');
10+
const editorHelpers = require('../lib/editor-helpers')
1011

1112
router.all('/*', (req, res, next) => {
1213
if (!req.user) {
@@ -23,28 +24,38 @@ router.get('/editor', passport.csrfProtection, (req, res) => {
2324
return res.redirect('/');
2425
}
2526

26-
resource.editorName = resource.editorName ||  'grapejs';
27-
resource.editorData = !resource.editorData ?
28-
{
27+
try {
28+
resource.editorData = JSON.parse(resource.editorData);
29+
} catch (err) {
30+
resource.editorData = {
2931
template: req.query.template || 'demo'
30-
} :
31-
JSON.parse(resource.editorData);
32+
}
33+
}
3234

33-
if (!resource.html && !resource.editorData.html) {
35+
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
36+
const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template);
3437
try {
35-
let file = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template, 'index.html');
36-
resource.html = fs.readFileSync(file, 'utf8');
38+
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
3739
} catch (err) {
38-
resource.html = err.message || err;
40+
try {
41+
resource.html = fs.readFileSync(path.join(base, 'index.html'), 'utf8');
42+
} catch (err) {
43+
resource.html = err.message || err;
44+
}
3945
}
4046
}
4147

4248
res.render('grapejs/editor', {
4349
layout: 'grapejs/layout-editor',
4450
type: req.query.type,
51+
stringifiedResource: JSON.stringify(resource),
4552
resource,
46-
editorConfig: config.grapejs,
47-
csrfToken: req.csrfToken(),
53+
editor: {
54+
name: resource.editorName || 'grapejs',
55+
mode: resource.editorData.mjml ? 'mjml' : 'html',
56+
config: config.grapejs
57+
},
58+
csrfToken: req.csrfToken()
4859
});
4960
});
5061
});

‎views/grapejs/editor.hbs

+227-130
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,7 @@
5959

6060

6161
<div id="gjs-wrapper">
62-
<div id="gjs" style="height:0px; overflow:hidden">
63-
{{#if resource.editorData.html}}
64-
<style>{{{resource.editorData.css}}}</style>
65-
{{{resource.editorData.html}}}
66-
{{else}}
67-
{{{resource.html}}}
68-
{{/if}}
69-
</div>
62+
<div id="gjs" style="height: 0px; overflow: hidden"></div>
7063
</div>
7164

7265

@@ -136,21 +129,69 @@
136129
<script>
137130
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
138131
139-
var editor = grapesjs.init({
140-
height: '100%',
141-
storageManager: {
142-
type: 'none'
143-
},
144-
assetManager: {
145-
assets: [],
146-
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}',
147-
uploadText: 'Drop images here or click to upload',
148-
},
149-
container : '#gjs',
150-
fromElement: true,
151-
plugins: ['gjs-preset-newsletter'],
152-
pluginsOpts: {
153-
'gjs-preset-newsletter': {
132+
var resource = {{{stringifiedResource}}};
133+
134+
var config = (function(mode) {
135+
var c = {
136+
clearOnRender: true,
137+
height: '100%',
138+
storageManager: {
139+
type: 'none'
140+
},
141+
assetManager: {
142+
assets: [],
143+
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}',
144+
uploadText: 'Drop images here or click to upload',
145+
},
146+
container : '#gjs',
147+
fromElement: false,
148+
plugins: [],
149+
pluginsOpts: {},
150+
};
151+
152+
if (mode === 'mjml') {
153+
var serializer = new XMLSerializer();
154+
var doc = new DOMParser().parseFromString(resource.editorData.mjml, 'text/xml');
155+
156+
// convert relative to absolute urls
157+
['mj-wrapper', 'mj-section', 'mj-navbar', 'mj-hero', 'mj-image'].forEach(function(tagName) {
158+
var serviceUrl = window.location.protocol + '//' + window.location.host + '/';
159+
var elements = doc.getElementsByTagName(tagName);
160+
161+
for (var i = 0; i < elements.length; i++) {
162+
var node = elements[i];
163+
var attrName = tagName === 'mj-image' ? 'src' : 'background-url';
164+
var url = node.getAttribute(attrName);
165+
166+
if (url && url.substring(0, 2) === './') {
167+
var absoluteUrl = serviceUrl + 'grapejs/templates/' + resource.editorData.template + '/' + url.substring(2);
168+
node.setAttribute(attrName, absoluteUrl);
169+
}
170+
}
171+
});
172+
173+
var title = doc.getElementsByTagName('mj-title')[0];
174+
if (title) {
175+
title.textContent = resource.name;
176+
}
177+
178+
var head = doc.getElementsByTagName('mj-head')[0];
179+
var mjHead = head ? serializer.serializeToString(head) : '<mj-head></mj-head>';
180+
181+
var container = doc.getElementsByTagName('mj-container')[0];
182+
var mjContainer = container ? serializer.serializeToString(container) : '<mj-container></mj-container>';
183+
184+
c.plugins.push('gjs-mjml', 'gjs-preset-mjml');
185+
c.pluginsOpts['gjs-mjml'] = {
186+
preMjml: '<mjml>' + mjHead + '<mj-body>',
187+
postMjml: '</mj-body></mjml>',
188+
};
189+
c.components = mjContainer;
190+
}
191+
192+
if (mode === 'html') {
193+
c.plugins.push('gjs-preset-newsletter');
194+
c.pluginsOpts['gjs-preset-newsletter'] = {
154195
modalLabelImport: 'Paste all your code here below and click import',
155196
modalLabelExport: 'Copy the code and use it wherever you want',
156197
codeViewerTheme: 'material',
@@ -163,68 +204,161 @@
163204
margin: 0,
164205
padding: 0,
165206
}
166-
}
207+
};
208+
c.components = resource.editorData.html || resource.html || '';
209+
c.style = resource.editorData.css || '';
167210
}
168-
});
169211
170-
$.getJSON('/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}', function(data) {
212+
return c;
213+
})('{{editor.mode}}');
214+
215+
var editor = grapesjs.init(config);
216+
217+
$.getJSON('/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}', function(data) {
171218
editor.AssetManager.add(data.files);
172219
});
173220
174-
function getPreparedHtml() {
175-
var imgs = [];
176-
$('.gjs-pn-buttons > .gjs-pn-btn.fa.fa-desktop').click();
177-
$('.gjs-editor > .gjs-cv-canvas > iframe.gjs-frame')
178-
.contents()
179-
.find('img')
180-
.each(function() {
181-
var src = $(this).attr('src');
182-
var s = src.match(/\/editorapi\/img\?src=([^&]*)/);
183-
var encodedSrc = (s && s[1]) || encodeURIComponent(src);
184-
var dynamicSrc = '/editorapi/img?src=' + encodedSrc + '&method=resize&params=' + $(this).width() + '%2C' + $(this).height();
185-
imgs.push({
186-
cls: $(this).attr('class').split(' ')[0],
187-
dynamicSrc: dynamicSrc,
188-
src: src,
221+
function getMjml() {
222+
var c = config.pluginsOpts['gjs-mjml'];
223+
return c.preMjml + editor.getHtml() + c.postMjml;
224+
}
225+
226+
function getPreparedHtml(callback) {
227+
var html;
228+
229+
switch ('{{editor.mode}}') {
230+
case 'html':
231+
html = editor.runCommand('gjs-get-inlined-html');
232+
html = '<!doctype html><html><head><meta charset="utf-8"><title>{{resource.name}}</title></head><body>' + html + '</body></html>';
233+
break;
234+
case 'mjml':
235+
var mjml = editor.runCommand('mjml-get-code');
236+
mjml.errors.length && mjml.errors.forEach(function(err) {
237+
console.warn(err.formattedMessage);
189238
});
190-
});
191-
var html = editor.runCommand('gjs-get-inlined-html');
192-
imgs.forEach(function(img) {
193-
html = html.replace(
194-
'<img class="' + img.cls + '" src="' + img.src,
195-
'<img class="' + img.cls + '" src="' + img.dynamicSrc
196-
);
197-
});
239+
html = mjml.html;
240+
break;
241+
}
242+
243+
var frame = document.createElement('iframe');
244+
frame.width = 2048;
245+
frame.height = 0;
246+
document.body.appendChild(frame);
247+
var frameDoc = frame.contentDocument || frame.contentWindow.document;
198248
199-
html = '<!doctype html><html><head><meta charset="utf-8"><title>{{resource.name}}</title></head><body>' + html + '</body></html>';
249+
frame.onload = function() {
250+
var imgs = frameDoc.querySelectorAll('img');
200251
201-
return html;
252+
for (var i = 0; i < imgs.length; i++) {
253+
var img = imgs[i];
254+
var m = img.src.match(/\/editorapi\/img\?src=([^&]*)/);
255+
var encodedSrc = m && m[1] || encodeURIComponent(img.src);
256+
img.src = '/editorapi/img?src=' + encodedSrc + '&method=resize&params=' + img.clientWidth + '%2C' + img.clientHeight;
257+
}
258+
259+
html = '<!doctype html>' + frameDoc.documentElement.outerHTML;
260+
document.body.removeChild(frame);
261+
callback(html);
262+
};
263+
264+
frameDoc.open();
265+
frameDoc.write(html);
266+
frameDoc.close();
202267
}
203268
269+
270+
// Save Button
271+
272+
window.bridge = window.bridge || {};
273+
274+
$('#mt-save').on('click', function() {
275+
276+
if ($(this).hasClass('busy')) {
277+
return;
278+
}
279+
280+
$(this).addClass('busy');
281+
282+
getPreparedHtml(function(html) {
283+
var editorData = '{{editor.mode}}' === 'mjml' ? {
284+
template: resource.editorData.template,
285+
mjml: getMjml(),
286+
} : {
287+
template: resource.editorData.template,
288+
css: editor.getCss(),
289+
html: editor.getHtml(),
290+
style: editor.getStyle(),
291+
components: editor.getComponents(),
292+
};
293+
294+
// TODO: Make templates and campaigns accept partial updates, i.e. don't require 'name' and 'list'
295+
var update = {
296+
id: resource.id,
297+
name: resource.name,
298+
{{#if resource.list}} list: resource.list, {{/if}}
299+
editorData: JSON.stringify(editorData),
300+
html: html,
301+
};
302+
303+
$.post('/editorapi/update?type={{type}}&editor={{editor.name}}', update, null, 'html')
304+
.success(function() {
305+
window.bridge.lastSavedHtml = html;
306+
toastr.success('Sucessfully saved');
307+
})
308+
.fail(function(data) {
309+
toastr.error(data.responseText || 'An error occured while saving the document');
310+
})
311+
.always(function() {
312+
setTimeout(function() {
313+
$('#mt-save').removeClass('busy');
314+
}, 200); // Don't save too fast
315+
});
316+
});
317+
});
318+
319+
320+
// Close Button
321+
322+
$('#mt-close').on('click', function() {
323+
if (confirm('Unsaved changes will be lost. Close now?') === true) {
324+
window.bridge.exit
325+
? window.bridge.exit()
326+
: window.location.href = '/{{type}}s/edit/{{resource.id}}?tab=template';
327+
}
328+
});
329+
330+
331+
// Commands
332+
204333
var mdlClass = 'gjs-mdl-dialog-sm';
205334
var pnm = editor.Panels;
206335
var cmdm = editor.Commands;
207-
var testContainer = document.getElementById("test-form");
208-
var contentEl = testContainer.querySelector('input[name=html]');
209336
var md = editor.Modal;
210337
338+
339+
// Test email command
340+
341+
var testContainer = document.getElementById('test-form');
342+
var testContentEl = testContainer.querySelector('input[name=html]');
343+
211344
cmdm.add('send-test', {
212345
run(editor, sender) {
213-
sender.set('active', 0);
214-
var modalContent = md.getContentEl();
215-
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
216-
// var cmdGetCode = cmdm.get('gjs-get-inlined-html');
217-
// contentEl.value = cmdGetCode && cmdGetCode.run(editor);
218-
contentEl.value = getPreparedHtml();
219-
mdlDialog.className += ' ' + mdlClass;
220-
testContainer.style.display = 'block';
221-
md.setTitle('Test your Newsletter');
222-
md.setContent(testContainer);
223-
md.open();
224-
md.getModel().once('change:open', function() {
225-
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
226-
//clean status
227-
})
346+
// TODO: Show a spinner
347+
getPreparedHtml(function(html) {
348+
sender.set('active', 0);
349+
var modalContent = md.getContentEl();
350+
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
351+
testContentEl.value = html;
352+
mdlDialog.className += ' ' + mdlClass;
353+
testContainer.style.display = 'block';
354+
md.setTitle('Test your Newsletter');
355+
md.setContent(testContainer);
356+
md.open();
357+
md.getModel().once('change:open', function() {
358+
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
359+
//clean status
360+
});
361+
});
228362
}
229363
});
230364
@@ -240,6 +374,7 @@
240374
241375
var statusFormElC = document.querySelector('.form-status');
242376
var statusFormEl = document.querySelector('.form-status i');
377+
243378
var ajaxTest = ajaxable(testContainer, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
244379
.onStart(function() {
245380
statusFormEl.className = 'fa fa-refresh anim-spin';
@@ -260,7 +395,24 @@
260395
}
261396
});
262397
263-
// Add Merge Tag Reference command
398+
// Remember testemail address
399+
400+
var isValidEmail = function(email) {
401+
return /\S+@\S+\.\S+/.test(email);
402+
};
403+
404+
if (isValidEmail(localStorage.getItem('testemail'))) {
405+
$('#test-form input[name=email]').val(localStorage.getItem('testemail'));
406+
}
407+
408+
$('#test-form').on('submit', function() {
409+
var email = $('#test-form input[name=email]').val();
410+
isValidEmail(email) && localStorage.setItem('testemail', email);
411+
});
412+
413+
414+
// Merge Tag Reference command
415+
264416
var mergeTagReferenceContainer = document.getElementById('merge-tag-reference-container');
265417
cmdm.add('open-merge-tag-reference', {
266418
run(editor, sender) {
@@ -287,7 +439,9 @@
287439
},
288440
});
289441
442+
290443
// Simple warn notifier
444+
291445
var origWarn = console.warn;
292446
toastr.options = {
293447
closeButton: true,
@@ -300,8 +454,10 @@
300454
origWarn(msg);
301455
};
302456
457+
458+
// Beautify tooltips
459+
303460
$(document).ready(function() {
304-
// Beautify tooltips
305461
$('*[title]').each(function() {
306462
var el = $(this);
307463
var title = el.attr('title').trim();
@@ -310,65 +466,6 @@
310466
el.attr('title', '');
311467
}
312468
});
313-
314-
// Remember testmail address
315-
var isValidEmail = function(email) {
316-
return /\S+@\S+\.\S+/.test(email);
317-
};
318-
var email = localStorage.getItem('testemail');
319-
isValidEmail(email) && $('#test-form input[name=email]').val(email);
320-
321-
$(document).on('submit', '#test-form', function() {
322-
var email = $('#test-form input[name=email]').val();
323-
isValidEmail(email) && localStorage.setItem('testemail', email);
324-
});
325-
326-
327-
// Save and Close Buttons
328-
329-
window.bridge = window.bridge || {};
330-
331-
$('#mt-close').on('click', function() {
332-
if (confirm('Unsaved changes will be lost. Close now?') === true) {
333-
window.bridge.exit
334-
? window.bridge.exit()
335-
: window.location.href = '/{{type}}s/edit/{{resource.id}}?tab=template';
336-
}
337-
});
338-
339-
$('#mt-save').on('click', function() {
340-
if ($(this).hasClass('busy')) {
341-
return;
342-
}
343-
$(this).addClass('busy');
344-
345-
var html = getPreparedHtml();
346-
347-
$.post('/editorapi/update?type={{type}}&editor={{resource.editorName}}', {
348-
id: {{resource.id}},
349-
name: '{{resource.name}}',
350-
{{#if resource.list}} list: {{resource.list}}, {{/if}}
351-
html: html,
352-
editorData: JSON.stringify({
353-
template: '{{resource.editorData.template}}',
354-
css: editor.getCss(),
355-
html: editor.getHtml(),
356-
style: editor.getStyle(),
357-
components: editor.getComponents(),
358-
}),
359-
}, null, 'html')
360-
.success(function() {
361-
window.bridge.lastSavedHtml = html;
362-
toastr.success('Sucessfully saved');
363-
})
364-
.fail(function(data) {
365-
toastr.error(data.responseText || 'An error occured while saving the document');
366-
})
367-
.always(function() {
368-
setTimeout(function() {
369-
$(this).removeClass('busy');
370-
}.bind(this), 500); // Don't save too fast
371-
}.bind(this));
372-
});
373469
});
470+
374471
</script>

‎views/grapejs/layout-editor.hbs

+18-7
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,28 @@
33
<head>
44
<meta charset="utf-8">
55
<title>GrapesJS Newsletter Editor</title>
6-
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.css">
6+
7+
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.css?v=0.5.41">
8+
<link rel="stylesheet" href="/grapejs/dist/css/toastr.min.css?v=2.1.3">
79
<link rel="stylesheet" href="/grapejs/dist/css/material.css">
810
<link rel="stylesheet" href="/grapejs/dist/css/tooltip.css">
9-
<link rel="stylesheet" href="/grapejs/dist/css/toastr.min.css">
10-
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-preset-newsletter.css">
1111

1212
<script src="/javascript/jquery-2.2.1.min.js"></script>
13-
<script src="/grapejs/dist/js/grapes.min.js"></script>
14-
<script src="/grapejs/dist/js/grapesjs-preset-newsletter.min.js"></script>
15-
<script src="/grapejs/dist/js/toastr.min.js"></script>
16-
<script src="/grapejs/dist/js/ajaxable.min.js"></script>
13+
<script src="/grapejs/dist/js/grapes.min.js?v=0.5.41"></script>
14+
<script src="/grapejs/dist/js/toastr.min.js?v=2.1.3"></script>
15+
<script src="/grapejs/dist/js/ajaxable.min.js?v=0.2.3"></script>
16+
17+
{{#switch editor.mode}}
18+
{{#case "mjml"}}
19+
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-mjml.css?v=0.0.7">
20+
<script src="/grapejs/dist/js/grapesjs-mjml.min.js?v=0.0.7"></script>
21+
<script src="/grapejs/dist/js/grapesjs-preset-mjml.js"></script>
22+
{{/case}}
23+
{{#case "html"}}
24+
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-preset-newsletter.css?v=0.2.3">
25+
<script src="/grapejs/dist/js/grapesjs-preset-newsletter.min.js?v=0.2.3"></script>
26+
{{/case}}
27+
{{/switch}}
1728
</head>
1829
<body>
1930

0 commit comments

Comments
 (0)
Please sign in to comment.