Merge branch 'master' of github.com:Mailtrain-org/mailtrain into access
This commit is contained in:
commit
d13fc65ce2
32 changed files with 1190 additions and 295 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -26,3 +26,4 @@ public/grapejs/uploads/*
|
||||||
!public/grapejs/uploads/README.md
|
!public/grapejs/uploads/README.md
|
||||||
public/grapejs/templates/*
|
public/grapejs/templates/*
|
||||||
!public/grapejs/templates/demo
|
!public/grapejs/templates/demo
|
||||||
|
!public/grapejs/templates/aves
|
||||||
|
|
18
.travis.yml
Normal file
18
.travis.yml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
dist: trusty
|
||||||
|
sudo: required
|
||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- 7
|
||||||
|
services:
|
||||||
|
- mysql
|
||||||
|
before_install:
|
||||||
|
- sudo apt-get -q -y install pwgen imagemagick
|
||||||
|
install:
|
||||||
|
- sudo bash test/e2e/install.sh
|
||||||
|
- npm install
|
||||||
|
before_script:
|
||||||
|
- npm run starttest > /dev/null 2>&1 &
|
||||||
|
- sleep 10
|
||||||
|
script:
|
||||||
|
- grunt
|
||||||
|
- npm run _e2e
|
|
@ -151,7 +151,10 @@ templates=[["versafix-1", "Versafix One"]]
|
||||||
|
|
||||||
[grapejs]
|
[grapejs]
|
||||||
# Installed templates
|
# Installed templates
|
||||||
templates=[["demo", "Demo Template"]]
|
templates=[
|
||||||
|
["demo", "HTML Template"],
|
||||||
|
["aves", "MJML Template"]
|
||||||
|
]
|
||||||
|
|
||||||
[reports]
|
[reports]
|
||||||
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
|
# The whole reporting functionality can be disabled below if the they are not needed and the DB cannot be
|
||||||
|
|
20
lib/db.js
20
lib/db.js
|
@ -4,6 +4,8 @@ let config = require('config');
|
||||||
let mysql = require('mysql');
|
let mysql = require('mysql');
|
||||||
let redis = require('redis');
|
let redis = require('redis');
|
||||||
let Lock = require('redfour');
|
let Lock = require('redfour');
|
||||||
|
let stringifyDate = require('json-stringify-date');
|
||||||
|
let tools = require('./tools');
|
||||||
|
|
||||||
module.exports = mysql.createPool(config.mysql);
|
module.exports = mysql.createPool(config.mysql);
|
||||||
if (config.redis && config.redis.enabled) {
|
if (config.redis && config.redis.enabled) {
|
||||||
|
@ -33,7 +35,7 @@ if (config.redis && config.redis.enabled) {
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.clearCache = (key, callback) => {
|
module.exports.clearCache = (key, callback) => {
|
||||||
module.exports.redis.del(key, err => callback(err));
|
module.exports.redis.del('mailtrain:cache:' + key, err => callback(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.addToCache = (key, value, callback) => {
|
module.exports.addToCache = (key, value, callback) => {
|
||||||
|
@ -41,7 +43,7 @@ if (config.redis && config.redis.enabled) {
|
||||||
return setImmediate(() => callback());
|
return setImmediate(() => callback());
|
||||||
}
|
}
|
||||||
module.exports.redis.multi().
|
module.exports.redis.multi().
|
||||||
lpush('mailtrain:cache:' + key, JSON.stringify(value)).
|
lpush('mailtrain:cache:' + key, stringifyDate.stringify(value)).
|
||||||
expire('mailtrain:cache:' + key, 24 * 3600).
|
expire('mailtrain:cache:' + key, 24 * 3600).
|
||||||
exec(err => callback(err));
|
exec(err => callback(err));
|
||||||
};
|
};
|
||||||
|
@ -52,7 +54,7 @@ if (config.redis && config.redis.enabled) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
value = JSON.parse(value);
|
value = stringifyDate.parse(value);
|
||||||
} catch (E) {
|
} catch (E) {
|
||||||
return callback(E);
|
return callback(E);
|
||||||
}
|
}
|
||||||
|
@ -76,9 +78,21 @@ if (config.redis && config.redis.enabled) {
|
||||||
|
|
||||||
module.exports.clearCache = (key, callback) => {
|
module.exports.clearCache = (key, callback) => {
|
||||||
caches.delete(key);
|
caches.delete(key);
|
||||||
|
tools.workers.forEach(child => {
|
||||||
|
child.send({
|
||||||
|
cmd: 'db.clearCache',
|
||||||
|
key
|
||||||
|
});
|
||||||
|
});
|
||||||
setImmediate(() => callback());
|
setImmediate(() => callback());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
process.on('message', m => {
|
||||||
|
if (m && m.cmd === 'db.clearCache' && m.key) {
|
||||||
|
caches.delete(m.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports.addToCache = (key, value, callback) => {
|
module.exports.addToCache = (key, value, callback) => {
|
||||||
if (!caches.has(key)) {
|
if (!caches.has(key)) {
|
||||||
caches.set(key, []);
|
caches.set(key, []);
|
||||||
|
|
|
@ -182,10 +182,10 @@ function createMailer(callback) {
|
||||||
module.exports.transport.checkThrottling = null;
|
module.exports.transport.checkThrottling = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let throttling = Number(configItems.smtpThrottling) || 0;
|
let sendingRate = Number(configItems.smtpThrottling) || 0;
|
||||||
if (throttling) {
|
if (sendingRate) {
|
||||||
// convert to messages/second
|
// convert to messages/second
|
||||||
throttling = 1 / (throttling / (3600 * 1000));
|
sendingRate = sendingRate / 3600;
|
||||||
}
|
}
|
||||||
|
|
||||||
let transportOptions;
|
let transportOptions;
|
||||||
|
@ -236,7 +236,7 @@ function createMailer(callback) {
|
||||||
error: logfunc.bind(null, 'error')
|
error: logfunc.bind(null, 'error')
|
||||||
},
|
},
|
||||||
maxConnections: Number(configItems.smtpMaxConnections),
|
maxConnections: Number(configItems.smtpMaxConnections),
|
||||||
sendingRate: throttling,
|
sendingRate,
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized: !configItems.smtpSelfSigned
|
rejectUnauthorized: !configItems.smtpSelfSigned
|
||||||
}
|
}
|
||||||
|
@ -257,19 +257,30 @@ function createMailer(callback) {
|
||||||
oldListeners.forEach(listener => module.exports.transport.on('idle', listener));
|
oldListeners.forEach(listener => module.exports.transport.on('idle', listener));
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastCheck = Date.now();
|
|
||||||
if (configItems.mailTransport === 'smtp' || !configItems.mailTransport) {
|
if (configItems.mailTransport === 'smtp' || !configItems.mailTransport) {
|
||||||
|
|
||||||
|
let throttling = Number(configItems.smtpThrottling) || 0;
|
||||||
|
if (throttling) {
|
||||||
|
throttling = 1 / (throttling / (3600 * 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastCheck = Date.now();
|
||||||
|
|
||||||
module.exports.transport.checkThrottling = function (next) {
|
module.exports.transport.checkThrottling = function (next) {
|
||||||
if (!throttling) {
|
if (!throttling) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
let nextCheck = Date.now();
|
let nextCheck = Date.now();
|
||||||
let checkDiff = (nextCheck - lastCheck);
|
let checkDiff = (nextCheck - lastCheck);
|
||||||
lastCheck = nextCheck;
|
|
||||||
if (checkDiff < throttling) {
|
if (checkDiff < throttling) {
|
||||||
log.verbose('Mail', 'Throttling next message in %s sec.', (throttling - checkDiff) / 1000);
|
log.verbose('Mail', 'Throttling next message in %s sec.', (throttling - checkDiff) / 1000);
|
||||||
setTimeout(next, throttling - checkDiff);
|
setTimeout(() => {
|
||||||
|
lastCheck = Date.now();
|
||||||
|
next();
|
||||||
|
}, throttling - checkDiff);
|
||||||
} else {
|
} else {
|
||||||
|
lastCheck = nextCheck;
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -458,7 +458,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||||
value: Number(valueList[field.column]) || 0,
|
value: Number(valueList[field.column]) || 0,
|
||||||
visible: !!field.visible,
|
visible: !!field.visible,
|
||||||
mergeTag: field.key,
|
mergeTag: field.key,
|
||||||
mergeValue: Number(valueList[field.column]) || Number(field.defaultValue) || 0,
|
mergeValue: (Number(valueList[field.column]) || Number(field.defaultValue) || 0).toString(),
|
||||||
['type' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
['type' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||||
};
|
};
|
||||||
row.push(item);
|
row.push(item);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
let db = require('../db');
|
let db = require('../db');
|
||||||
let shortid = require('shortid');
|
let shortid = require('shortid');
|
||||||
|
let striptags = require('striptags');
|
||||||
let tools = require('../tools');
|
let tools = require('../tools');
|
||||||
let helpers = require('../helpers');
|
let helpers = require('../helpers');
|
||||||
let fields = require('./fields');
|
let fields = require('./fields');
|
||||||
|
@ -136,6 +137,8 @@ module.exports.insert = (listId, meta, subscriptionData, callback) => {
|
||||||
values.push(field.value);
|
values.push(field.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
values = values.map(v => typeof v === 'string' ? striptags(v) : v);
|
||||||
|
|
||||||
db.getConnection((err, connection) => {
|
db.getConnection((err, connection) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
@ -355,7 +358,7 @@ module.exports.getWithMergeTags = (listId, cid, callback) => {
|
||||||
TIMEZONE: subscription.tz || ''
|
TIMEZONE: subscription.tz || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
fields.getRow(fieldList, subscription, true, true).forEach(field => {
|
fields.getRow(fieldList, subscription, false, true).forEach(field => {
|
||||||
if (field.mergeTag) {
|
if (field.mergeTag) {
|
||||||
subscription.mergeTags[field.mergeTag] = field.mergeValue || '';
|
subscription.mergeTags[field.mergeTag] = field.mergeValue || '';
|
||||||
}
|
}
|
||||||
|
@ -420,6 +423,8 @@ module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
||||||
return callback(null, false);
|
return callback(null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
values = values.map(v => typeof v === 'string' ? striptags(v) : v);
|
||||||
|
|
||||||
db.getConnection((err, connection) => {
|
db.getConnection((err, connection) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
|
19
lib/tools.js
19
lib/tools.js
|
@ -13,6 +13,7 @@ let he = require('he');
|
||||||
let _ = require('./translate')._;
|
let _ = require('./translate')._;
|
||||||
let util = require('util');
|
let util = require('util');
|
||||||
let createDOMPurify = require('dompurify');
|
let createDOMPurify = require('dompurify');
|
||||||
|
let htmlToText = require('html-to-text');
|
||||||
|
|
||||||
let blockedUsers = ['abuse', 'admin', 'billing', 'compliance', 'devnull', 'dns', 'ftp', 'hostmaster', 'inoc', 'ispfeedback', 'ispsupport', 'listrequest', 'list', 'maildaemon', 'noc', 'noreply', 'noreply', 'null', 'phish', 'phishing', 'postmaster', 'privacy', 'registrar', 'root', 'security', 'spam', 'support', 'sysadmin', 'tech', 'undisclosedrecipients', 'unsubscribe', 'usenet', 'uucp', 'webmaster', 'www'];
|
let blockedUsers = ['abuse', 'admin', 'billing', 'compliance', 'devnull', 'dns', 'ftp', 'hostmaster', 'inoc', 'ispfeedback', 'ispsupport', 'listrequest', 'list', 'maildaemon', 'noc', 'noreply', 'noreply', 'null', 'phish', 'phishing', 'postmaster', 'privacy', 'registrar', 'root', 'security', 'spam', 'support', 'sysadmin', 'tech', 'undisclosedrecipients', 'unsubscribe', 'usenet', 'uucp', 'webmaster', 'www'];
|
||||||
|
|
||||||
|
@ -200,17 +201,24 @@ function formatMessage(serviceUrl, campaign, list, subscription, message, filter
|
||||||
return links[key];
|
return links[key];
|
||||||
}
|
}
|
||||||
if (subscription.mergeTags.hasOwnProperty(key)) {
|
if (subscription.mergeTags.hasOwnProperty(key)) {
|
||||||
return isHTML ? he.encode((subscription.mergeTags[key] || ''), {
|
let value = (subscription.mergeTags[key] || '').toString();
|
||||||
useNamedReferences: true
|
let containsHTML = /<[a-z][\s\S]*>/.test(value);
|
||||||
}) : subscription.mergeTags[key];
|
return isHTML ? he.encode((containsHTML ? value : value.replace(/(?:\r\n|\r|\n)/g, '<br/>')), {
|
||||||
|
useNamedReferences: true,
|
||||||
|
allowUnsafeSymbols: true
|
||||||
|
}) : (containsHTML ? htmlToText.fromString(value) : value);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
return message.replace(/\[([a-z0-9_]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
|
return message.replace(/\[([a-z0-9_]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
|
||||||
identifier = identifier.toUpperCase();
|
identifier = identifier.toUpperCase();
|
||||||
let value = (getValue(identifier) || fallback || '').trim();
|
let value = getValue(identifier);
|
||||||
return value ? filter(value) : match;
|
if (value === false) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
value = (value || fallback || '').trim();
|
||||||
|
return filter(value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,4 +318,3 @@ function mergeTemplateIntoLayout(template, layout, callback) {
|
||||||
return done(template, layout);
|
return done(template, layout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,7 @@
|
||||||
"isemail": "^2.2.1",
|
"isemail": "^2.2.1",
|
||||||
"jquery-file-upload-middleware": "^0.1.8",
|
"jquery-file-upload-middleware": "^0.1.8",
|
||||||
"jsdom": "^9.12.0",
|
"jsdom": "^9.12.0",
|
||||||
|
"json-stringify-date": "^0.1.4",
|
||||||
"juice": "^4.0.2",
|
"juice": "^4.0.2",
|
||||||
"knex": "^0.13.0",
|
"knex": "^0.13.0",
|
||||||
"libmime": "^3.1.0",
|
"libmime": "^3.1.0",
|
||||||
|
|
2
public/grapejs/dist/css/grapes.min.css
vendored
2
public/grapejs/dist/css/grapes.min.css
vendored
File diff suppressed because one or more lines are too long
248
public/grapejs/dist/css/grapesjs-mjml.css
vendored
Normal file
248
public/grapejs/dist/css/grapesjs-mjml.css
vendored
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
.gjs-clm-tags .gjs-sm-title,
|
||||||
|
.gjs-sm-sector .gjs-sm-title {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-clm-tags .gjs-clm-tag {
|
||||||
|
/* background-color: $tag-color; */
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 5px 8px;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-field {
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-btnt.gjs-pn-active,
|
||||||
|
.gjs-pn-btn.gjs-pn-active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-pn-btn:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-btnt.gjs-pn-active,
|
||||||
|
.gjs-color-active,
|
||||||
|
.gjs-pn-btn.gjs-pn-active,
|
||||||
|
.gjs-pn-btn:active,
|
||||||
|
.gjs-block:hover {
|
||||||
|
color: #f45e43; /* #f46d4c,#e4505d */
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-rte-toolbar .gjs-rte-btn,
|
||||||
|
.gjs-btn-prim,
|
||||||
|
.gjs-btnt,
|
||||||
|
.gjs-clm-tags .gjs-sm-composite.gjs-clm-field,
|
||||||
|
.gjs-clm-tags .gjs-sm-field.gjs-sm-composite,
|
||||||
|
.gjs-clm-tags .gjs-sm-stack #gjs-sm-add,
|
||||||
|
.gjs-color-main,
|
||||||
|
.gjs-mdl-dialog,
|
||||||
|
.gjs-off-prv,
|
||||||
|
.gjs-pn-btn,
|
||||||
|
.gjs-pn-panel,
|
||||||
|
.gjs-sm-sector .gjs-sm-composite.gjs-clm-field,
|
||||||
|
.gjs-sm-sector .gjs-sm-field.gjs-sm-composite,
|
||||||
|
.gjs-sm-sector .gjs-sm-stack #gjs-sm-add {
|
||||||
|
color: #888686;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-rte-toolbar,
|
||||||
|
.gjs-bg-main,
|
||||||
|
.gjs-clm-select option,
|
||||||
|
.gjs-clm-tags .gjs-sm-colorp-c,
|
||||||
|
.gjs-editor,
|
||||||
|
.gjs-mdl-dialog,
|
||||||
|
.gjs-nv-item .gjs-nv-title-c,
|
||||||
|
.gjs-off-prv,
|
||||||
|
.gjs-pn-panel,
|
||||||
|
.gjs-select option,
|
||||||
|
.gjs-sm-sector .gjs-sm-colorp-c,
|
||||||
|
.gjs-sm-select option,
|
||||||
|
.gjs-sm-unit option,
|
||||||
|
.sp-container,
|
||||||
|
.gjs-block {
|
||||||
|
background-color: #2c2e35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-import-label,
|
||||||
|
.gjs-export-label {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-mdl-dialog .gjs-btn-import {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror {
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 450px;
|
||||||
|
font-family: sans-serif, monospace;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra */
|
||||||
|
|
||||||
|
.gjs-block {
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 10px 2.5% 5px;
|
||||||
|
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
||||||
|
transition: box-shadow 0.2s ease 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-block:hover {
|
||||||
|
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-pn-views-container.gjs-pn-panel {
|
||||||
|
padding: 39px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-pn-views.gjs-pn-panel {
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-pn-views .gjs-pn-btn {
|
||||||
|
margin: 0;
|
||||||
|
height: 40px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 25%;
|
||||||
|
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-pn-views .gjs-pn-active {
|
||||||
|
/*
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
border-bottom: 2px solid #f45e43;
|
||||||
|
*/
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-pn-devices-c {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-pn-options {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-sm-composite .gjs-sm-properties {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-sm-border-top-left-radius,
|
||||||
|
#gjs-sm-border-top-right-radius,
|
||||||
|
#gjs-sm-border-bottom-left-radius,
|
||||||
|
#gjs-sm-border-bottom-right-radius,
|
||||||
|
#gjs-sm-margin-top,
|
||||||
|
#gjs-sm-margin-bottom,
|
||||||
|
#gjs-sm-margin-right,
|
||||||
|
#gjs-sm-margin-left,
|
||||||
|
#gjs-sm-padding-top,
|
||||||
|
#gjs-sm-padding-bottom,
|
||||||
|
#gjs-sm-padding-right,
|
||||||
|
#gjs-sm-padding-left {
|
||||||
|
flex: 999 1 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-sm-border-width,
|
||||||
|
#gjs-sm-border-style,
|
||||||
|
#gjs-sm-border-color {
|
||||||
|
flex: 999 1 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-sm-margin-left,
|
||||||
|
#gjs-sm-padding-left {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-sm-margin-right,
|
||||||
|
#gjs-sm-padding-right {
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#gjs-sm-margin-bottom,
|
||||||
|
#gjs-sm-padding-bottom {
|
||||||
|
order: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-field-radio {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-field-radio #gjs-sm-input-holder {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-radio-item {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-sm-sector .gjs-sm-property.gjs-sm-list {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-mdl-content {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-sm-sector .gjs-sm-property .gjs-sm-layer.gjs-sm-active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
#gjs-pn-views-container,
|
||||||
|
#gjs-pn-views{
|
||||||
|
min-width: 270px;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.gjs-f-button::before { content: 'B'; }
|
||||||
|
.gjs-f-divider::before { content: 'D'; }
|
||||||
|
|
||||||
|
.gjs-mdl-dialog-sm {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-mdl-dialog form .gjs-sm-property {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gjs-mdl-dialog form .gjs-sm-label {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anim-spin {
|
||||||
|
animation: 0.5s linear 0s normal none infinite running spin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-status {
|
||||||
|
float: right;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #f92929;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -54,14 +54,13 @@
|
||||||
.gjs-nv-item .gjs-nv-title-c,
|
.gjs-nv-item .gjs-nv-title-c,
|
||||||
.gjs-off-prv,
|
.gjs-off-prv,
|
||||||
.gjs-pn-panel,
|
.gjs-pn-panel,
|
||||||
|
.gjs-block,
|
||||||
.gjs-select option,
|
.gjs-select option,
|
||||||
.gjs-sm-sector .gjs-sm-colorp-c,
|
.gjs-sm-sector .gjs-sm-colorp-c,
|
||||||
.gjs-sm-select option,
|
.gjs-sm-select option,
|
||||||
.gjs-sm-unit option,
|
.gjs-sm-unit option,
|
||||||
.sp-container,
|
.sp-container {
|
||||||
.gjs-block {
|
background-color: #373d49; }
|
||||||
background-color: #373d49;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gjs-import-label,
|
.gjs-import-label,
|
||||||
.gjs-export-label {
|
.gjs-export-label {
|
||||||
|
@ -79,18 +78,15 @@
|
||||||
font-size: 12px; }
|
font-size: 12px; }
|
||||||
|
|
||||||
/* Extra */
|
/* Extra */
|
||||||
|
|
||||||
.gjs-block {
|
.gjs-block {
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
margin: 10px 2.5% 5px;
|
margin: 10px 2.5% 5px;
|
||||||
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
|
||||||
transition: box-shadow 0.2s ease 0s;
|
transition: box-shadow, color 0.2s ease 0s; }
|
||||||
}
|
|
||||||
|
|
||||||
.gjs-block:hover {
|
.gjs-block:hover {
|
||||||
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15);
|
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15); }
|
||||||
}
|
|
||||||
|
|
||||||
#gjs-pn-views-container.gjs-pn-panel {
|
#gjs-pn-views-container.gjs-pn-panel {
|
||||||
padding: 39px 0 0; }
|
padding: 39px 0 0; }
|
||||||
|
|
34
public/grapejs/dist/js/grapes.min.js
vendored
34
public/grapejs/dist/js/grapes.min.js
vendored
File diff suppressed because one or more lines are too long
108
public/grapejs/dist/js/grapesjs-mjml.min.js
vendored
Normal file
108
public/grapejs/dist/js/grapesjs-mjml.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
124
public/grapejs/dist/js/grapesjs-preset-mjml.js
vendored
Normal file
124
public/grapejs/dist/js/grapesjs-preset-mjml.js
vendored
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
grapesjs.plugins.add('gjs-preset-mjml', (editor, opts) => {
|
||||||
|
var opt = opts || {};
|
||||||
|
var config = editor.getConfig();
|
||||||
|
|
||||||
|
config.showDevices = 0;
|
||||||
|
|
||||||
|
var updateTooltip = function(coll, pos) {
|
||||||
|
coll.each(function(item) {
|
||||||
|
var attrs = item.get('attributes');
|
||||||
|
attrs['data-tooltip-pos'] = pos || 'bottom';
|
||||||
|
item.set('attributes', attrs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/****************** COMMANDS *************************/
|
||||||
|
|
||||||
|
var cmdm = editor.Commands;
|
||||||
|
cmdm.add('undo', {
|
||||||
|
run: function(editor, sender) {
|
||||||
|
sender.set('active', 0);
|
||||||
|
editor.UndoManager.undo(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cmdm.add('redo', {
|
||||||
|
run: function(editor, sender) {
|
||||||
|
sender.set('active', 0);
|
||||||
|
editor.UndoManager.redo(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cmdm.add('set-device-desktop', {
|
||||||
|
run: function(editor) {
|
||||||
|
editor.setDevice('Desktop');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cmdm.add('set-device-mobile', {
|
||||||
|
run: function(editor) {
|
||||||
|
editor.setDevice('Mobile');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cmdm.add('clean-all', {
|
||||||
|
run: function(editor, sender) {
|
||||||
|
sender && sender.set('active',false);
|
||||||
|
if (confirm('Are you sure you want to clean the canvas?')) {
|
||||||
|
editor.setComponents('<mj-container><mj-section><mj-column>'+
|
||||||
|
'<mj-text>Start from here</mj-text></mj-column></mj-section></mj-container>');
|
||||||
|
localStorage.setItem('gjs-mjml-css', '');
|
||||||
|
localStorage.setItem('gjs-mjml-html', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/****************** BUTTONS *************************/
|
||||||
|
|
||||||
|
var pnm = editor.Panels;
|
||||||
|
pnm.addButton('options', [{
|
||||||
|
id: 'undo',
|
||||||
|
className: 'fa fa-undo icon-undo',
|
||||||
|
command: 'undo',
|
||||||
|
attributes: { title: 'Undo (CTRL/CMD + Z)'}
|
||||||
|
},{
|
||||||
|
id: 'redo',
|
||||||
|
className: 'fa fa-repeat icon-redo',
|
||||||
|
command: 'redo',
|
||||||
|
attributes: { title: 'Redo (CTRL/CMD + SHIFT + Z)' }
|
||||||
|
},{
|
||||||
|
id: 'clean-all',
|
||||||
|
className: 'fa fa-trash icon-blank',
|
||||||
|
command: 'clean-all',
|
||||||
|
attributes: { title: 'Empty canvas' }
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Add devices buttons
|
||||||
|
var panelDevices = pnm.addPanel({id: 'devices-c'});
|
||||||
|
var deviceBtns = panelDevices.get('buttons');
|
||||||
|
deviceBtns.add([{
|
||||||
|
id: 'deviceDesktop',
|
||||||
|
command: 'set-device-desktop',
|
||||||
|
className: 'fa fa-desktop',
|
||||||
|
attributes: {'title': 'Desktop'},
|
||||||
|
active: 1,
|
||||||
|
},{
|
||||||
|
id: 'deviceMobile',
|
||||||
|
command: 'set-device-mobile',
|
||||||
|
className: 'fa fa-mobile',
|
||||||
|
attributes: {'title': 'Mobile'},
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Remove preview and code button
|
||||||
|
let prvBtn = pnm.addButton('options', 'preview');
|
||||||
|
let optPanel = pnm.getPanel('options');
|
||||||
|
let cmdBtns = optPanel.get('buttons');
|
||||||
|
prvBtn && cmdBtns.remove(prvBtn);
|
||||||
|
|
||||||
|
updateTooltip(deviceBtns);
|
||||||
|
updateTooltip(pnm.getPanel('options').get('buttons'));
|
||||||
|
updateTooltip(pnm.getPanel('options').get('buttons'));
|
||||||
|
updateTooltip(pnm.getPanel('views').get('buttons'));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/****************** EVENTS *************************/
|
||||||
|
|
||||||
|
// On component change show the Style Manager
|
||||||
|
editor.on('change:selectedComponent', function() {
|
||||||
|
var openLayersBtn = editor.Panels.getButton('views', 'open-layers');
|
||||||
|
|
||||||
|
// Don't switch when the Layer Manager is on or
|
||||||
|
// there is no selected component
|
||||||
|
if((!openLayersBtn || !openLayersBtn.get('active')) &&
|
||||||
|
editor.editor.get('selectedComponent')) {
|
||||||
|
var openSmBtn = editor.Panels.getButton('views', 'open-sm');
|
||||||
|
openSmBtn && openSmBtn.set('active', 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do stuff on load
|
||||||
|
editor.on('load', function() {
|
||||||
|
// Open block manager
|
||||||
|
var openBlocksBtn = editor.Panels.getButton('views', 'open-blocks');
|
||||||
|
openBlocksBtn && openBlocksBtn.set('active', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
File diff suppressed because one or more lines are too long
1
public/grapejs/dist/js/toastr.min.js
vendored
1
public/grapejs/dist/js/toastr.min.js
vendored
File diff suppressed because one or more lines are too long
9
public/grapejs/templates/aves/CREDITS.md
Normal file
9
public/grapejs/templates/aves/CREDITS.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#### Image Credits
|
||||||
|
|
||||||
|
All images CC0. Thanks to the Pixabay community.
|
||||||
|
|
||||||
|
* [bird-1.jpg](https://pixabay.com/de/zoo-pfau-kopf-tier-federn-vogel-866181/)
|
||||||
|
* [bird-2.jpg](https://pixabay.com/de/möwe-himmel-urlaub-vogel-schnabel-249638/)
|
||||||
|
* [bird-3.jpg](https://pixabay.com/de/porträt-vogel-natur-wild-räuber-1072696/)
|
||||||
|
* [bird-4.jpg](https://pixabay.com/de/goldener-fasan-vogel-exotische-317503/)
|
||||||
|
* [clouds.jpg](https://pixabay.com/de/luft-atmosphäre-blau-klar-klima-2716/)
|
BIN
public/grapejs/templates/aves/images/bird-1.jpg
Normal file
BIN
public/grapejs/templates/aves/images/bird-1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
BIN
public/grapejs/templates/aves/images/bird-2.jpg
Normal file
BIN
public/grapejs/templates/aves/images/bird-2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
BIN
public/grapejs/templates/aves/images/bird-3.jpg
Normal file
BIN
public/grapejs/templates/aves/images/bird-3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 104 KiB |
BIN
public/grapejs/templates/aves/images/bird-4.jpg
Normal file
BIN
public/grapejs/templates/aves/images/bird-4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 145 KiB |
BIN
public/grapejs/templates/aves/images/clouds.jpg
Normal file
BIN
public/grapejs/templates/aves/images/clouds.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
86
public/grapejs/templates/aves/index.mjml
Normal file
86
public/grapejs/templates/aves/index.mjml
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
<mjml>
|
||||||
|
<mj-head>
|
||||||
|
<mj-title>Hello World</mj-title>
|
||||||
|
</mj-head>
|
||||||
|
<mj-body>
|
||||||
|
<mj-container>
|
||||||
|
|
||||||
|
<!-- Company Header -->
|
||||||
|
<mj-section background-color="#f0f0f0">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-style="italic" font-size="20" color="#626262">
|
||||||
|
My Company
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<!-- Image Header -->
|
||||||
|
<mj-section background-url="./images/clouds.jpg" background-size="cover" background-repeat="no-repeat">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text align="center" color="#fff" font-size="40" font-family="Helvetica Neue" padding-top="30px" padding-bottom="30px">Slogan here</mj-text>
|
||||||
|
<mj-button background-color="#F63A4D" href="#">
|
||||||
|
Promotion
|
||||||
|
</mj-button>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<!-- Intro text -->
|
||||||
|
<mj-section background-color="#fafafa">
|
||||||
|
<mj-column width="400">
|
||||||
|
<mj-text font-style="italic" font-size="20" font-family="Helvetica Neue" color="#cc2d2d">Attention!</mj-text>
|
||||||
|
<mj-text color="#525252">
|
||||||
|
The MJML Mode is currently experimental and there're a few bugs you should avoid.
|
||||||
|
<ul>
|
||||||
|
<li>Don't toggle visibility in the layer manager.</li>
|
||||||
|
<li>Don't import the mj-head if you import a custom template.</li>
|
||||||
|
<li>Don't duplicate mj-image as the duplicate is missing the src attribute on export. This bug is probably not limited to mj-image.</li>
|
||||||
|
<li>Don't use % values.</li>
|
||||||
|
</ul>
|
||||||
|
It's generally working great and very promising. Thanks to @artf for making this all possible.
|
||||||
|
</mj-text>
|
||||||
|
<mj-button background-color="#F45E43" href="https://github.com/artf/grapesjs-mjml">Learn more about GrapesJS MJML</mj-button>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<!-- Side image -->
|
||||||
|
<mj-section background-color="white">
|
||||||
|
<mj-column>
|
||||||
|
<mj-image src="./images/bird-1.jpg" />
|
||||||
|
</mj-column>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-style="italic" font-size="20" font-family="Helvetica Neue" color="#626262">
|
||||||
|
Amazing Birds
|
||||||
|
</mj-text>
|
||||||
|
<mj-text color="#525252">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus lectus.</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<!-- Icons -->
|
||||||
|
<mj-section background-color="#fbfbfb">
|
||||||
|
<mj-column>
|
||||||
|
<mj-image width="200" src="./images/bird-2.jpg" />
|
||||||
|
</mj-column>
|
||||||
|
<mj-column>
|
||||||
|
<mj-image width="200" src="./images/bird-3.jpg" />
|
||||||
|
</mj-column>
|
||||||
|
<mj-column>
|
||||||
|
<mj-image width="200" src="./images/bird-4.jpg" />
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<mj-section background-color="#e7e7e7">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text color="#525252">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin rutrum enim eget magna efficitur, eu semper augue semper. Aliquam erat volutpat. Cras id dui lectus. Vestibulum sed finibus lectus, sit amet suscipit nibh. Proin nec commodo purus. Sed eget
|
||||||
|
nulla elit. Nulla aliquet mollis faucibus.
|
||||||
|
</mj-text>
|
||||||
|
<mj-button href="#">Hello There!</mj-button>
|
||||||
|
<mj-social display="facebook twitter" />
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
</mj-container>
|
||||||
|
</mj-body>
|
||||||
|
</mjml>
|
|
@ -545,7 +545,7 @@ router.post('/clicked/ajax/:id/:linkId', (req, res) => {
|
||||||
let campaignCid = campaign.cid;
|
let campaignCid = campaign.cid;
|
||||||
let listCid = list.cid;
|
let listCid = list.cid;
|
||||||
|
|
||||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '`.`created', 'count'];
|
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '.created', 'count'];
|
||||||
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
@ -634,7 +634,7 @@ router.post('/status/ajax/:id/:status', (req, res) => {
|
||||||
let campaignCid = campaign.cid;
|
let campaignCid = campaign.cid;
|
||||||
let listCid = list.cid;
|
let listCid = list.cid;
|
||||||
|
|
||||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign__' + campaign.id + '`.`updated'];
|
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign__' + campaign.id + '.updated'];
|
||||||
campaigns.filterStatusSubscribers(campaign, status, req.body, columns, (err, data, total, filteredTotal) => {
|
campaigns.filterStatusSubscribers(campaign, status, req.body, columns, (err, data, total, filteredTotal) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
@ -683,7 +683,7 @@ router.post('/clicked/ajax/:id/:linkId', (req, res) => {
|
||||||
let campaignCid = campaign.cid;
|
let campaignCid = campaign.cid;
|
||||||
let listCid = list.cid;
|
let listCid = list.cid;
|
||||||
|
|
||||||
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '`.`created', 'count'];
|
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '.created', 'count'];
|
||||||
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let config = require('config');
|
const config = require('config');
|
||||||
let express = require('express');
|
const express = require('express');
|
||||||
let router = new express.Router();
|
const router = new express.Router();
|
||||||
let passport = require('../lib/passport');
|
const passport = require('../lib/passport');
|
||||||
let fs = require('fs');
|
const _ = require('../lib/translate')._;
|
||||||
let path = require('path');
|
const fs = require('fs');
|
||||||
let editorHelpers = require('../lib/editor-helpers.js')
|
const path = require('path');
|
||||||
|
const editorHelpers = require('../lib/editor-helpers')
|
||||||
|
|
||||||
router.all('/*', (req, res, next) => {
|
router.all('/*', (req, res, next) => {
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
|
@ -23,28 +24,38 @@ router.get('/editor', passport.csrfProtection, (req, res) => {
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
resource.editorName = resource.editorName || 'grapejs';
|
try {
|
||||||
resource.editorData = !resource.editorData ?
|
resource.editorData = JSON.parse(resource.editorData);
|
||||||
{
|
} catch (err) {
|
||||||
|
resource.editorData = {
|
||||||
template: req.query.template || 'demo'
|
template: req.query.template || 'demo'
|
||||||
} :
|
}
|
||||||
JSON.parse(resource.editorData);
|
}
|
||||||
|
|
||||||
if (!resource.html && !resource.editorData.html) {
|
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
|
||||||
|
const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template);
|
||||||
try {
|
try {
|
||||||
let file = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template, 'index.html');
|
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
|
||||||
resource.html = fs.readFileSync(file, 'utf8');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resource.html = err.message || err;
|
try {
|
||||||
|
resource.html = fs.readFileSync(path.join(base, 'index.html'), 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
resource.html = err.message || err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('grapejs/editor', {
|
res.render('grapejs/editor', {
|
||||||
layout: 'grapejs/layout-editor',
|
layout: 'grapejs/layout-editor',
|
||||||
type: req.query.type,
|
type: req.query.type,
|
||||||
|
stringifiedResource: JSON.stringify(resource),
|
||||||
resource,
|
resource,
|
||||||
editorConfig: config.grapejs,
|
editor: {
|
||||||
csrfToken: req.csrfToken(),
|
name: resource.editorName || 'grapejs',
|
||||||
|
mode: resource.editorData.mjml ? 'mjml' : 'html',
|
||||||
|
config: config.grapejs
|
||||||
|
},
|
||||||
|
csrfToken: req.csrfToken()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -257,7 +257,7 @@ router.post('/status/ajax/:id', (req, res) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let columns = ['#', 'email', 'first_name', 'last_name', 'trigger__' + trigger.id + '`.`created'];
|
let columns = ['#', 'email', 'first_name', 'last_name', 'trigger__' + trigger.id + '.created'];
|
||||||
triggers.filterSubscribers(trigger, req.body, columns, (err, data, total, filteredTotal) => {
|
triggers.filterSubscribers(trigger, req.body, columns, (err, data, total, filteredTotal) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|
|
@ -332,7 +332,7 @@ function formatMessage(message, callback) {
|
||||||
};
|
};
|
||||||
|
|
||||||
let encryptionKeys = [];
|
let encryptionKeys = [];
|
||||||
fields.getRow(fieldList, message.subscription, true, true).forEach(field => {
|
fields.getRow(fieldList, message.subscription, false, true).forEach(field => {
|
||||||
if (field.mergeTag) {
|
if (field.mergeTag) {
|
||||||
message.subscription.mergeTags[field.mergeTag] = field.mergeValue || '';
|
message.subscription.mergeTags[field.mergeTag] = field.mergeValue || '';
|
||||||
}
|
}
|
||||||
|
@ -466,119 +466,129 @@ let sendLoop = () => {
|
||||||
return setTimeout(sendLoop, 10 * 1000);
|
return setTimeout(sendLoop, 10 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isThrottled = false;
|
||||||
|
|
||||||
let getNext = () => {
|
let getNext = () => {
|
||||||
if (!mailer.transport.isIdle()) {
|
if (!mailer.transport.isIdle() || isThrottled) {
|
||||||
// only retrieve new messages if there are free slots in the mailer queue
|
// only retrieve new messages if there are free slots in the mailer queue
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find an unsent message
|
isThrottled = true;
|
||||||
findUnsent((err, message) => {
|
|
||||||
if (err) {
|
|
||||||
log.error('Mail', err.stack);
|
|
||||||
setTimeout(getNext, mailing_timeout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!message) {
|
|
||||||
setTimeout(getNext, mailing_timeout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//log.verbose('Mail', 'Found new message to be delivered: %s', message.subscription.cid);
|
mailer.transport.checkThrottling(() => {
|
||||||
// format message to nodemailer message format
|
|
||||||
formatMessage(message, (err, mail) => {
|
isThrottled = false;
|
||||||
|
|
||||||
|
// find an unsent message
|
||||||
|
findUnsent((err, message) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Mail', err.stack);
|
log.error('Mail', err.stack);
|
||||||
setTimeout(getNext, mailing_timeout);
|
setTimeout(getNext, mailing_timeout);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!message) {
|
||||||
|
setTimeout(getNext, mailing_timeout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
blacklist.isblacklisted(mail.to.address, (err, blacklisted) => {
|
// log.verbose('Mail', 'Found new message to be delivered: %s', message.subscription.cid);
|
||||||
if (err) {
|
// format message to nodemailer message format
|
||||||
log.error('Mail', err);
|
formatMessage(message, (err, mail) => {
|
||||||
setTimeout(getNext, mailing_timeout);
|
if (err) {
|
||||||
return;
|
log.error('Mail', err.stack);
|
||||||
}
|
setTimeout(getNext, mailing_timeout);
|
||||||
if (!blacklisted) {
|
return;
|
||||||
let tryCount = 0;
|
}
|
||||||
let trySend = () => {
|
|
||||||
tryCount++;
|
|
||||||
|
|
||||||
// send the message
|
blacklist.isblacklisted(mail.to.address, (err, blacklisted) => {
|
||||||
mailer.transport.sendMail(mail, (err, info) => {
|
if (err) {
|
||||||
if (err) {
|
log.error('Mail', err);
|
||||||
log.error('Mail', err.stack);
|
setTimeout(getNext, mailing_timeout);
|
||||||
if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
|
return;
|
||||||
// temporary error, try again
|
}
|
||||||
return setTimeout(trySend, tryCount * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let status = err ? 2 : 1;
|
if (!blacklisted) {
|
||||||
let response = err && (err.response || err.message) || info.response || info.messageId;
|
let tryCount = 0;
|
||||||
let responseId = response.split(/\s+/).pop();
|
let trySend = () => {
|
||||||
|
tryCount++;
|
||||||
|
|
||||||
|
// send the message
|
||||||
|
mailer.transport.sendMail(mail, (err, info) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('Mail', err.stack);
|
||||||
|
if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
|
||||||
|
// temporary error, try again
|
||||||
|
return setTimeout(trySend, tryCount * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = err ? 2 : 1;
|
||||||
|
let response = err && (err.response || err.message) || info.response || info.messageId;
|
||||||
|
let responseId = response.split(/\s+/).pop();
|
||||||
|
|
||||||
|
db.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('Mail', err.stack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = 'UPDATE `campaigns` SET `delivered`=`delivered`+1 ' + (status === 2 ? ', `bounced`=`bounced`+1 ' : '') + ' WHERE id=? LIMIT 1';
|
||||||
|
|
||||||
|
connection.query(query, [message.campaignId], err => {
|
||||||
|
if (err) {
|
||||||
|
log.error('Mail', err.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
|
||||||
|
|
||||||
|
connection.query(query, [status, response, responseId, message.id], err => {
|
||||||
|
connection.release();
|
||||||
|
if (err) {
|
||||||
|
log.error('Mail', err.stack);
|
||||||
|
} else {
|
||||||
|
// log.verbose('Mail', 'Message sent and status updated for %s', message.subscription.cid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
setImmediate(trySend);
|
||||||
|
} else {
|
||||||
db.getConnection((err, connection) => {
|
db.getConnection((err, connection) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Mail', err.stack);
|
log.error('Mail', err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = 'UPDATE `campaigns` SET `delivered`=`delivered`+1 ' + (status === 2 ? ', `bounced`=`bounced`+1 ' : '') + ' WHERE id=? LIMIT 1';
|
let query = 'UPDATE `campaigns` SET `blacklisted`=`blacklisted`+1 WHERE id=? LIMIT 1';
|
||||||
|
|
||||||
connection.query(query, [message.campaignId], err => {
|
connection.query(query, [message.campaignId], err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Mail', err.stack);
|
log.error('Mail', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
|
let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
|
||||||
|
|
||||||
connection.query(query, [status, response, responseId, message.id], err => {
|
connection.query(query, [5, 'blacklisted', 'blacklisted', message.id], err => {
|
||||||
connection.release();
|
connection.release();
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Mail', err.stack);
|
log.error('Mail', err);
|
||||||
} else {
|
|
||||||
// log.verbose('Mail', 'Message sent and status updated for %s', message.subscription.cid);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
};
|
setImmediate(getNext);
|
||||||
setImmediate(trySend);
|
});
|
||||||
} else {
|
|
||||||
db.getConnection((err, connection) => {
|
|
||||||
if (err) {
|
|
||||||
log.error('Mail', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = 'UPDATE `campaigns` SET `blacklisted`=`blacklisted`+1 WHERE id=? LIMIT 1';
|
|
||||||
|
|
||||||
connection.query(query, [message.campaignId], err => {
|
|
||||||
if (err) {
|
|
||||||
log.error('Mail', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
|
|
||||||
|
|
||||||
connection.query(query, [5, 'blacklisted', 'blacklisted', message.id], err => {
|
|
||||||
connection.release();
|
|
||||||
if (err) {
|
|
||||||
log.error('Mail', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setImmediate(() => mailer.transport.checkThrottling(getNext));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
mailer.transport.on('idle', () => mailer.transport.checkThrottling(getNext));
|
mailer.transport.on('idle', getNext);
|
||||||
setImmediate(() => mailer.transport.checkThrottling(getNext));
|
setImmediate(getNext);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -436,4 +436,75 @@ suite('Subscription use-cases', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useCase('A subscriber address can be changed to an address which has been previously unsubscribed. #222', async () => {
|
||||||
|
const page = getPage(config.lists.l1);
|
||||||
|
|
||||||
|
const oldSubscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||||
|
email: generateEmail(),
|
||||||
|
firstName: 'old first name',
|
||||||
|
lastName: 'old last name'
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the unsubscribe button.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows a notice that confirms unsubscription.', async () => {
|
||||||
|
await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email that confirms unsubscription.', async () => {
|
||||||
|
await page.mailUnsubscriptionConfirmed.fetchMail(oldSubscription.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newSubscription = await subscriptionExistsPrecondition(config.lists.l1, {
|
||||||
|
email: generateEmail(),
|
||||||
|
firstName: 'new first name'
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the manage subscription button.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.click('manageLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks the change address button.', async () => {
|
||||||
|
await page.webManage.click('manageAddressLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('Systems shows a form to change email.', async () => {
|
||||||
|
await page.webManageAddress.waitUntilVisibleAfterRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User fills in the email address of the original subscription and submits the form.', async () => {
|
||||||
|
await page.webManageAddress.setValue('emailNewInput', oldSubscription.email);
|
||||||
|
await page.webManageAddress.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System goes back to the profile form and shows a flash notice that further instructions are in the email.', async () => {
|
||||||
|
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||||
|
await page.webManage.waitForFlash();
|
||||||
|
expect(await page.webManage.getFlash()).to.contain('An email with further instructions has been sent to the provided address');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with a link to confirm the address change.', async () => {
|
||||||
|
await page.mailConfirmAddressChange.fetchMail(oldSubscription.email);
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('User clicks confirm subscription in the email', async () => {
|
||||||
|
await page.mailConfirmAddressChange.click('confirmLink');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System shows the profile form with a flash notice that address has been changed. The form does not contain data from the old subscription.', async () => {
|
||||||
|
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||||
|
await page.webManage.waitForFlash();
|
||||||
|
expect(await page.webManage.getFlash()).to.contain('Email address changed');
|
||||||
|
expect(await page.webManage.getValue('emailInput')).to.equal(oldSubscription.email);
|
||||||
|
expect(await page.webManage.getValue('firstNameInput')).to.equal(newSubscription.firstName);
|
||||||
|
expect(await page.webManage.getValue('lastNameInput')).to.equal('');
|
||||||
|
});
|
||||||
|
|
||||||
|
await step('System sends an email with subscription confirmation.', async () => {
|
||||||
|
await page.mailSubscriptionConfirmed.fetchMail(oldSubscription.email);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,14 +59,7 @@
|
||||||
|
|
||||||
|
|
||||||
<div id="gjs-wrapper">
|
<div id="gjs-wrapper">
|
||||||
<div id="gjs" style="height:0px; overflow:hidden">
|
<div id="gjs" style="height: 0px; overflow: hidden"></div>
|
||||||
{{#if resource.editorData.html}}
|
|
||||||
<style>{{{resource.editorData.css}}}</style>
|
|
||||||
{{{resource.editorData.html}}}
|
|
||||||
{{else}}
|
|
||||||
{{{resource.html}}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -136,21 +129,69 @@
|
||||||
<script>
|
<script>
|
||||||
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
|
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
|
||||||
|
|
||||||
var editor = grapesjs.init({
|
var resource = {{{stringifiedResource}}};
|
||||||
height: '100%',
|
|
||||||
storageManager: {
|
var config = (function(mode) {
|
||||||
type: 'none'
|
var c = {
|
||||||
},
|
clearOnRender: true,
|
||||||
assetManager: {
|
height: '100%',
|
||||||
assets: [],
|
storageManager: {
|
||||||
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}',
|
type: 'none'
|
||||||
uploadText: 'Drop images here or click to upload',
|
},
|
||||||
},
|
assetManager: {
|
||||||
container : '#gjs',
|
assets: [],
|
||||||
fromElement: true,
|
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}',
|
||||||
plugins: ['gjs-preset-newsletter'],
|
uploadText: 'Drop images here or click to upload',
|
||||||
pluginsOpts: {
|
},
|
||||||
'gjs-preset-newsletter': {
|
container : '#gjs',
|
||||||
|
fromElement: false,
|
||||||
|
plugins: [],
|
||||||
|
pluginsOpts: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mode === 'mjml') {
|
||||||
|
var serializer = new XMLSerializer();
|
||||||
|
var doc = new DOMParser().parseFromString(resource.editorData.mjml, 'text/xml');
|
||||||
|
|
||||||
|
// convert relative to absolute urls
|
||||||
|
['mj-wrapper', 'mj-section', 'mj-navbar', 'mj-hero', 'mj-image'].forEach(function(tagName) {
|
||||||
|
var serviceUrl = window.location.protocol + '//' + window.location.host + '/';
|
||||||
|
var elements = doc.getElementsByTagName(tagName);
|
||||||
|
|
||||||
|
for (var i = 0; i < elements.length; i++) {
|
||||||
|
var node = elements[i];
|
||||||
|
var attrName = tagName === 'mj-image' ? 'src' : 'background-url';
|
||||||
|
var url = node.getAttribute(attrName);
|
||||||
|
|
||||||
|
if (url && url.substring(0, 2) === './') {
|
||||||
|
var absoluteUrl = serviceUrl + 'grapejs/templates/' + resource.editorData.template + '/' + url.substring(2);
|
||||||
|
node.setAttribute(attrName, absoluteUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var title = doc.getElementsByTagName('mj-title')[0];
|
||||||
|
if (title) {
|
||||||
|
title.textContent = resource.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
var head = doc.getElementsByTagName('mj-head')[0];
|
||||||
|
var mjHead = head ? serializer.serializeToString(head) : '<mj-head></mj-head>';
|
||||||
|
|
||||||
|
var container = doc.getElementsByTagName('mj-container')[0];
|
||||||
|
var mjContainer = container ? serializer.serializeToString(container) : '<mj-container></mj-container>';
|
||||||
|
|
||||||
|
c.plugins.push('gjs-mjml', 'gjs-preset-mjml');
|
||||||
|
c.pluginsOpts['gjs-mjml'] = {
|
||||||
|
preMjml: '<mjml>' + mjHead + '<mj-body>',
|
||||||
|
postMjml: '</mj-body></mjml>',
|
||||||
|
};
|
||||||
|
c.components = mjContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'html') {
|
||||||
|
c.plugins.push('gjs-preset-newsletter');
|
||||||
|
c.pluginsOpts['gjs-preset-newsletter'] = {
|
||||||
modalLabelImport: 'Paste all your code here below and click import',
|
modalLabelImport: 'Paste all your code here below and click import',
|
||||||
modalLabelExport: 'Copy the code and use it wherever you want',
|
modalLabelExport: 'Copy the code and use it wherever you want',
|
||||||
codeViewerTheme: 'material',
|
codeViewerTheme: 'material',
|
||||||
|
@ -163,68 +204,161 @@
|
||||||
margin: 0,
|
margin: 0,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
c.components = resource.editorData.html || resource.html || '';
|
||||||
|
c.style = resource.editorData.css || '';
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
$.getJSON('/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}', function(data) {
|
return c;
|
||||||
|
})('{{editor.mode}}');
|
||||||
|
|
||||||
|
var editor = grapesjs.init(config);
|
||||||
|
|
||||||
|
$.getJSON('/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}', function(data) {
|
||||||
editor.AssetManager.add(data.files);
|
editor.AssetManager.add(data.files);
|
||||||
});
|
});
|
||||||
|
|
||||||
function getPreparedHtml() {
|
function getMjml() {
|
||||||
var imgs = [];
|
var c = config.pluginsOpts['gjs-mjml'];
|
||||||
$('.gjs-pn-buttons > .gjs-pn-btn.fa.fa-desktop').click();
|
return c.preMjml + editor.getHtml() + c.postMjml;
|
||||||
$('.gjs-editor > .gjs-cv-canvas > iframe.gjs-frame')
|
}
|
||||||
.contents()
|
|
||||||
.find('img')
|
function getPreparedHtml(callback) {
|
||||||
.each(function() {
|
var html;
|
||||||
var src = $(this).attr('src');
|
|
||||||
var s = src.match(/\/editorapi\/img\?src=([^&]*)/);
|
switch ('{{editor.mode}}') {
|
||||||
var encodedSrc = (s && s[1]) || encodeURIComponent(src);
|
case 'html':
|
||||||
var dynamicSrc = '/editorapi/img?src=' + encodedSrc + '&method=resize¶ms=' + $(this).width() + '%2C' + $(this).height();
|
html = editor.runCommand('gjs-get-inlined-html');
|
||||||
imgs.push({
|
html = '<!doctype html><html><head><meta charset="utf-8"><title>{{resource.name}}</title></head><body>' + html + '</body></html>';
|
||||||
cls: $(this).attr('class').split(' ')[0],
|
break;
|
||||||
dynamicSrc: dynamicSrc,
|
case 'mjml':
|
||||||
src: src,
|
var mjml = editor.runCommand('mjml-get-code');
|
||||||
|
mjml.errors.length && mjml.errors.forEach(function(err) {
|
||||||
|
console.warn(err.formattedMessage);
|
||||||
|
});
|
||||||
|
html = mjml.html;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frame = document.createElement('iframe');
|
||||||
|
frame.width = 2048;
|
||||||
|
frame.height = 0;
|
||||||
|
document.body.appendChild(frame);
|
||||||
|
var frameDoc = frame.contentDocument || frame.contentWindow.document;
|
||||||
|
|
||||||
|
frame.onload = function() {
|
||||||
|
var imgs = frameDoc.querySelectorAll('img');
|
||||||
|
|
||||||
|
for (var i = 0; i < imgs.length; i++) {
|
||||||
|
var img = imgs[i];
|
||||||
|
var m = img.src.match(/\/editorapi\/img\?src=([^&]*)/);
|
||||||
|
var encodedSrc = m && m[1] || encodeURIComponent(img.src);
|
||||||
|
img.src = '/editorapi/img?src=' + encodedSrc + '&method=resize¶ms=' + img.clientWidth + '%2C' + img.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
html = '<!doctype html>' + frameDoc.documentElement.outerHTML;
|
||||||
|
document.body.removeChild(frame);
|
||||||
|
callback(html);
|
||||||
|
};
|
||||||
|
|
||||||
|
frameDoc.open();
|
||||||
|
frameDoc.write(html);
|
||||||
|
frameDoc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Save Button
|
||||||
|
|
||||||
|
window.bridge = window.bridge || {};
|
||||||
|
|
||||||
|
$('#mt-save').on('click', function() {
|
||||||
|
|
||||||
|
if ($(this).hasClass('busy')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).addClass('busy');
|
||||||
|
|
||||||
|
getPreparedHtml(function(html) {
|
||||||
|
var editorData = '{{editor.mode}}' === 'mjml' ? {
|
||||||
|
template: resource.editorData.template,
|
||||||
|
mjml: getMjml(),
|
||||||
|
} : {
|
||||||
|
template: resource.editorData.template,
|
||||||
|
css: editor.getCss(),
|
||||||
|
html: editor.getHtml(),
|
||||||
|
style: editor.getStyle(),
|
||||||
|
components: editor.getComponents(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Make templates and campaigns accept partial updates, i.e. don't require 'name' and 'list'
|
||||||
|
var update = {
|
||||||
|
id: resource.id,
|
||||||
|
name: resource.name,
|
||||||
|
{{#if resource.list}} list: resource.list, {{/if}}
|
||||||
|
editorData: JSON.stringify(editorData),
|
||||||
|
html: html,
|
||||||
|
};
|
||||||
|
|
||||||
|
$.post('/editorapi/update?type={{type}}&editor={{editor.name}}', update, null, 'html')
|
||||||
|
.success(function() {
|
||||||
|
window.bridge.lastSavedHtml = html;
|
||||||
|
toastr.success('Sucessfully saved');
|
||||||
|
})
|
||||||
|
.fail(function(data) {
|
||||||
|
toastr.error(data.responseText || 'An error occured while saving the document');
|
||||||
|
})
|
||||||
|
.always(function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
$('#mt-save').removeClass('busy');
|
||||||
|
}, 200); // Don't save too fast
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
var html = editor.runCommand('gjs-get-inlined-html');
|
});
|
||||||
imgs.forEach(function(img) {
|
|
||||||
html = html.replace(
|
|
||||||
'<img class="' + img.cls + '" src="' + img.src,
|
|
||||||
'<img class="' + img.cls + '" src="' + img.dynamicSrc
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
html = '<!doctype html><html><head><meta charset="utf-8"><title>{{resource.name}}</title></head><body>' + html + '</body></html>';
|
|
||||||
|
|
||||||
return html;
|
// Close Button
|
||||||
}
|
|
||||||
|
$('#mt-close').on('click', function() {
|
||||||
|
if (confirm('Unsaved changes will be lost. Close now?') === true) {
|
||||||
|
window.bridge.exit
|
||||||
|
? window.bridge.exit()
|
||||||
|
: window.location.href = '/{{type}}s/edit/{{resource.id}}?tab=template';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
|
||||||
var mdlClass = 'gjs-mdl-dialog-sm';
|
var mdlClass = 'gjs-mdl-dialog-sm';
|
||||||
var pnm = editor.Panels;
|
var pnm = editor.Panels;
|
||||||
var cmdm = editor.Commands;
|
var cmdm = editor.Commands;
|
||||||
var testContainer = document.getElementById("test-form");
|
|
||||||
var contentEl = testContainer.querySelector('input[name=html]');
|
|
||||||
var md = editor.Modal;
|
var md = editor.Modal;
|
||||||
|
|
||||||
|
|
||||||
|
// Test email command
|
||||||
|
|
||||||
|
var testContainer = document.getElementById('test-form');
|
||||||
|
var testContentEl = testContainer.querySelector('input[name=html]');
|
||||||
|
|
||||||
cmdm.add('send-test', {
|
cmdm.add('send-test', {
|
||||||
run(editor, sender) {
|
run(editor, sender) {
|
||||||
sender.set('active', 0);
|
// TODO: Show a spinner
|
||||||
var modalContent = md.getContentEl();
|
getPreparedHtml(function(html) {
|
||||||
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
|
sender.set('active', 0);
|
||||||
// var cmdGetCode = cmdm.get('gjs-get-inlined-html');
|
var modalContent = md.getContentEl();
|
||||||
// contentEl.value = cmdGetCode && cmdGetCode.run(editor);
|
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
|
||||||
contentEl.value = getPreparedHtml();
|
testContentEl.value = html;
|
||||||
mdlDialog.className += ' ' + mdlClass;
|
mdlDialog.className += ' ' + mdlClass;
|
||||||
testContainer.style.display = 'block';
|
testContainer.style.display = 'block';
|
||||||
md.setTitle('Test your Newsletter');
|
md.setTitle('Test your Newsletter');
|
||||||
md.setContent(testContainer);
|
md.setContent(testContainer);
|
||||||
md.open();
|
md.open();
|
||||||
md.getModel().once('change:open', function() {
|
md.getModel().once('change:open', function() {
|
||||||
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
|
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
|
||||||
//clean status
|
//clean status
|
||||||
})
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -240,6 +374,7 @@
|
||||||
|
|
||||||
var statusFormElC = document.querySelector('.form-status');
|
var statusFormElC = document.querySelector('.form-status');
|
||||||
var statusFormEl = document.querySelector('.form-status i');
|
var statusFormEl = document.querySelector('.form-status i');
|
||||||
|
|
||||||
var ajaxTest = ajaxable(testContainer, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
|
var ajaxTest = ajaxable(testContainer, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
|
||||||
.onStart(function() {
|
.onStart(function() {
|
||||||
statusFormEl.className = 'fa fa-refresh anim-spin';
|
statusFormEl.className = 'fa fa-refresh anim-spin';
|
||||||
|
@ -260,7 +395,24 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add Merge Tag Reference command
|
// Remember testemail address
|
||||||
|
|
||||||
|
var isValidEmail = function(email) {
|
||||||
|
return /\S+@\S+\.\S+/.test(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isValidEmail(localStorage.getItem('testemail'))) {
|
||||||
|
$('#test-form input[name=email]').val(localStorage.getItem('testemail'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#test-form').on('submit', function() {
|
||||||
|
var email = $('#test-form input[name=email]').val();
|
||||||
|
isValidEmail(email) && localStorage.setItem('testemail', email);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Merge Tag Reference command
|
||||||
|
|
||||||
var mergeTagReferenceContainer = document.getElementById('merge-tag-reference-container');
|
var mergeTagReferenceContainer = document.getElementById('merge-tag-reference-container');
|
||||||
cmdm.add('open-merge-tag-reference', {
|
cmdm.add('open-merge-tag-reference', {
|
||||||
run(editor, sender) {
|
run(editor, sender) {
|
||||||
|
@ -287,7 +439,9 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Simple warn notifier
|
// Simple warn notifier
|
||||||
|
|
||||||
var origWarn = console.warn;
|
var origWarn = console.warn;
|
||||||
toastr.options = {
|
toastr.options = {
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
|
@ -300,8 +454,10 @@
|
||||||
origWarn(msg);
|
origWarn(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Beautify tooltips
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
// Beautify tooltips
|
|
||||||
$('*[title]').each(function() {
|
$('*[title]').each(function() {
|
||||||
var el = $(this);
|
var el = $(this);
|
||||||
var title = el.attr('title').trim();
|
var title = el.attr('title').trim();
|
||||||
|
@ -310,65 +466,6 @@
|
||||||
el.attr('title', '');
|
el.attr('title', '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remember testmail address
|
|
||||||
var isValidEmail = function(email) {
|
|
||||||
return /\S+@\S+\.\S+/.test(email);
|
|
||||||
};
|
|
||||||
var email = localStorage.getItem('testemail');
|
|
||||||
isValidEmail(email) && $('#test-form input[name=email]').val(email);
|
|
||||||
|
|
||||||
$(document).on('submit', '#test-form', function() {
|
|
||||||
var email = $('#test-form input[name=email]').val();
|
|
||||||
isValidEmail(email) && localStorage.setItem('testemail', email);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
// Save and Close Buttons
|
|
||||||
|
|
||||||
window.bridge = window.bridge || {};
|
|
||||||
|
|
||||||
$('#mt-close').on('click', function() {
|
|
||||||
if (confirm('Unsaved changes will be lost. Close now?') === true) {
|
|
||||||
window.bridge.exit
|
|
||||||
? window.bridge.exit()
|
|
||||||
: window.location.href = '/{{type}}s/edit/{{resource.id}}?tab=template';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#mt-save').on('click', function() {
|
|
||||||
if ($(this).hasClass('busy')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$(this).addClass('busy');
|
|
||||||
|
|
||||||
var html = getPreparedHtml();
|
|
||||||
|
|
||||||
$.post('/editorapi/update?type={{type}}&editor={{resource.editorName}}', {
|
|
||||||
id: {{resource.id}},
|
|
||||||
name: '{{resource.name}}',
|
|
||||||
{{#if resource.list}} list: {{resource.list}}, {{/if}}
|
|
||||||
html: html,
|
|
||||||
editorData: JSON.stringify({
|
|
||||||
template: '{{resource.editorData.template}}',
|
|
||||||
css: editor.getCss(),
|
|
||||||
html: editor.getHtml(),
|
|
||||||
style: editor.getStyle(),
|
|
||||||
components: editor.getComponents(),
|
|
||||||
}),
|
|
||||||
}, null, 'html')
|
|
||||||
.success(function() {
|
|
||||||
window.bridge.lastSavedHtml = html;
|
|
||||||
toastr.success('Sucessfully saved');
|
|
||||||
})
|
|
||||||
.fail(function(data) {
|
|
||||||
toastr.error(data.responseText || 'An error occured while saving the document');
|
|
||||||
})
|
|
||||||
.always(function() {
|
|
||||||
setTimeout(function() {
|
|
||||||
$(this).removeClass('busy');
|
|
||||||
}.bind(this), 500); // Don't save too fast
|
|
||||||
}.bind(this));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,17 +3,28 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>GrapesJS Newsletter Editor</title>
|
<title>GrapesJS Newsletter Editor</title>
|
||||||
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.css">
|
|
||||||
|
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.css?v=0.5.41">
|
||||||
|
<link rel="stylesheet" href="/grapejs/dist/css/toastr.min.css?v=2.1.3">
|
||||||
<link rel="stylesheet" href="/grapejs/dist/css/material.css">
|
<link rel="stylesheet" href="/grapejs/dist/css/material.css">
|
||||||
<link rel="stylesheet" href="/grapejs/dist/css/tooltip.css">
|
<link rel="stylesheet" href="/grapejs/dist/css/tooltip.css">
|
||||||
<link rel="stylesheet" href="/grapejs/dist/css/toastr.min.css">
|
|
||||||
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-preset-newsletter.css">
|
|
||||||
|
|
||||||
<script src="/javascript/jquery-2.2.1.min.js"></script>
|
<script src="/javascript/jquery-2.2.1.min.js"></script>
|
||||||
<script src="/grapejs/dist/js/grapes.min.js"></script>
|
<script src="/grapejs/dist/js/grapes.min.js?v=0.5.41"></script>
|
||||||
<script src="/grapejs/dist/js/grapesjs-preset-newsletter.min.js"></script>
|
<script src="/grapejs/dist/js/toastr.min.js?v=2.1.3"></script>
|
||||||
<script src="/grapejs/dist/js/toastr.min.js"></script>
|
<script src="/grapejs/dist/js/ajaxable.min.js?v=0.2.3"></script>
|
||||||
<script src="/grapejs/dist/js/ajaxable.min.js"></script>
|
|
||||||
|
{{#switch editor.mode}}
|
||||||
|
{{#case "mjml"}}
|
||||||
|
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-mjml.css?v=0.0.7">
|
||||||
|
<script src="/grapejs/dist/js/grapesjs-mjml.min.js?v=0.0.7"></script>
|
||||||
|
<script src="/grapejs/dist/js/grapesjs-preset-mjml.js"></script>
|
||||||
|
{{/case}}
|
||||||
|
{{#case "html"}}
|
||||||
|
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-preset-newsletter.css?v=0.2.3">
|
||||||
|
<script src="/grapejs/dist/js/grapesjs-preset-newsletter.min.js?v=0.2.3"></script>
|
||||||
|
{{/case}}
|
||||||
|
{{/switch}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue