GrapeJS and Mosaico Integration
This commit is contained in:
parent
c36071524d
commit
588eed008b
13 changed files with 1493 additions and 18 deletions
68
lib/editor-helpers.js
Normal file
68
lib/editor-helpers.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
'use strict';
|
||||
|
||||
let _ = require('../lib/translate')._;
|
||||
let helpers = require('../lib/helpers');
|
||||
let templates = require('../lib/models/templates');
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
|
||||
module.exports = {
|
||||
getResource
|
||||
};
|
||||
|
||||
function getResource(type, id, callback) {
|
||||
if (type === 'template') {
|
||||
templates.get(id, (err, template) => {
|
||||
if (err || !template) {
|
||||
return callback(err && err.message || err || _('Could not find template with specified ID'));
|
||||
}
|
||||
|
||||
getMergeTagsForResource(template, (err, mergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
template.mergeTags = mergeTags;
|
||||
return callback(null, template);
|
||||
});
|
||||
});
|
||||
|
||||
} else if (type === 'campaign') {
|
||||
campaigns.get(id, false, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
return callback(err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
}
|
||||
|
||||
getMergeTagsForResource(campaign, (err, mergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
campaign.mergeTags = mergeTags;
|
||||
return callback(null, campaign);
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
return callback(_('Invalid resource type'));
|
||||
}
|
||||
}
|
||||
|
||||
function getMergeTagsForResource(resource, callback) {
|
||||
helpers.getDefaultMergeTags((err, defaultMergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
if (!resource.list) {
|
||||
return callback(null, defaultMergeTags);
|
||||
}
|
||||
|
||||
helpers.getListMergeTags(resource.list, (err, listMergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
callback(null, defaultMergeTags.concat(listMergeTags));
|
||||
});
|
||||
});
|
||||
}
|
433
routes/editorapi.js
Normal file
433
routes/editorapi.js
Normal file
|
@ -0,0 +1,433 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let os = require('os');
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let mkdirp = require('mkdirp');
|
||||
let cache = require('memory-cache');
|
||||
let crypto = require('crypto');
|
||||
let fetch = require('node-fetch');
|
||||
let events = require('events');
|
||||
let httpMocks = require('node-mocks-http');
|
||||
let multiparty = require('multiparty');
|
||||
let fileType = require('file-type');
|
||||
let escapeStringRegexp = require('escape-string-regexp');
|
||||
let jqueryFileUpload = require('jquery-file-upload-middleware');
|
||||
let gm = require('gm').subClass({ imageMagick: true });
|
||||
let url = require('url');
|
||||
let htmlToText = require('html-to-text');
|
||||
let premailerApi = require('premailer-api');
|
||||
let editorHelpers = require('../lib/editor-helpers');
|
||||
let _ = require('../lib/translate')._;
|
||||
let mailer = require('../lib/mailer');
|
||||
let settings = require('../lib/models/settings');
|
||||
let templates = require('../lib/models/templates');
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user && !cache.get(req.get('If-Match'))) {
|
||||
return res.status(403).send(_('Need to be logged in to access restricted content'));
|
||||
}
|
||||
if (req.originalUrl.startsWith('/editorapi/img?')) {
|
||||
return next();
|
||||
}
|
||||
if (!config.editors.map(e => e[0]).includes(req.query.editor)) {
|
||||
return res.status(500).send(_('Invalid editor name'));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
jqueryFileUpload.on('begin', fileInfo => {
|
||||
fileInfo.name = fileInfo.name
|
||||
.toLowerCase()
|
||||
.replace(/ /g, '-')
|
||||
.replace(/[^a-z0-9+-\.]+/g, '');
|
||||
});
|
||||
|
||||
let listImages = (dir, dirURL, callback) => {
|
||||
fs.readdir(dir, (err, files = []) => {
|
||||
if (err && err.code !== 'ENOENT') {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
files = files.filter(name => /\.(jpe?g|png|gif)$/i.test(name));
|
||||
files = files.map(name => {
|
||||
return {
|
||||
// mosaico
|
||||
name,
|
||||
url: dirURL + '/' + name,
|
||||
thumbnailUrl: dirURL + '/thumbnail/' + name,
|
||||
// grapejs
|
||||
src: dirURL + '/' + name,
|
||||
};
|
||||
});
|
||||
callback(null, files);
|
||||
});
|
||||
};
|
||||
|
||||
let getStaticImageUrl = ({ dynamicUrl, staticDir, staticDirUrl }, callback) => {
|
||||
mkdirp(staticDir, err => {
|
||||
if (err) {
|
||||
return callback(dynamicUrl);
|
||||
}
|
||||
|
||||
fs.readdir(staticDir, (err, files) => {
|
||||
if (err) {
|
||||
return callback(dynamicUrl);
|
||||
}
|
||||
|
||||
let hash = crypto.createHash('md5').update(dynamicUrl).digest('hex');
|
||||
let match = files.find(el => el.startsWith(hash));
|
||||
let headers = {};
|
||||
|
||||
if (match) {
|
||||
return callback(staticDirUrl + '/' + match);
|
||||
}
|
||||
|
||||
if (dynamicUrl.includes('/editorapi/img?')) {
|
||||
let token = crypto.randomBytes(16).toString('hex');
|
||||
cache.put(token, true, 1000);
|
||||
headers['If-Match'] = token;
|
||||
}
|
||||
|
||||
fetch(dynamicUrl, {
|
||||
headers
|
||||
})
|
||||
.then(res => {
|
||||
return res.buffer();
|
||||
})
|
||||
.then(buffer => {
|
||||
let ft = fileType(buffer);
|
||||
if (!ft) {
|
||||
return callback(dynamicUrl);
|
||||
}
|
||||
if (['image/jpeg', 'image/png', 'image/gif'].includes(ft.mime)) {
|
||||
fs.writeFile(path.join(staticDir, hash + '.' + ft.ext), buffer, err => {
|
||||
if (err) {
|
||||
return callback(dynamicUrl);
|
||||
}
|
||||
let staticUrl = staticDirUrl + '/' + hash + '.' + ft.ext;
|
||||
callback(staticUrl);
|
||||
});
|
||||
} else {
|
||||
callback(dynamicUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let prepareHtml = ({ editorName, html }, callback) => {
|
||||
settings.get('serviceUrl', (err, serviceUrl) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
let jobs = 0;
|
||||
let srcs = {};
|
||||
let re = /<img[^>]+src="([^"]+)"/g;
|
||||
let result;
|
||||
while ((result = re.exec(html)) !== null) {
|
||||
srcs[result[1]] = result[1];
|
||||
}
|
||||
|
||||
let done = () => {
|
||||
if (jobs === 0) {
|
||||
Object.keys(srcs).forEach(src => {
|
||||
// console.log(`replace dynamic - ${src} - with static - ${srcs[src]}`);
|
||||
html = html.replace(new RegExp(escapeStringRegexp(src), 'g'), srcs[src]);
|
||||
});
|
||||
callback(null, html);
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(srcs).forEach(src => {
|
||||
jobs++;
|
||||
let dynamicUrl = src.replace(/&/g, '&');
|
||||
dynamicUrl = /^https?:\/\/|^\/\//i.test(dynamicUrl) ? dynamicUrl : url.resolve(serviceUrl, dynamicUrl);
|
||||
getStaticImageUrl({
|
||||
dynamicUrl,
|
||||
staticDir: path.join(__dirname, '..', 'public', editorName, 'uploads', 'static'),
|
||||
staticDirUrl: url.resolve(serviceUrl, editorName + '/uploads/static'),
|
||||
}, staticUrl => {
|
||||
srcs[src] = staticUrl;
|
||||
jobs--;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
let placeholderImage = (req, res, { width, height }) => {
|
||||
let magick = gm(width, height, '#707070');
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let size = 40;
|
||||
// stripes
|
||||
while (y < height) {
|
||||
magick = magick
|
||||
.fill('#808080')
|
||||
.drawPolygon([x, y], [x + size, y], [x + size * 2, y + size], [x + size * 2, y + size * 2])
|
||||
.drawPolygon([x, y + size], [x + size, y + size * 2], [x, y + size * 2]);
|
||||
x = x + size * 2;
|
||||
if (x > width) {
|
||||
x = 0;
|
||||
y = y + size * 2;
|
||||
}
|
||||
}
|
||||
// text
|
||||
magick = magick
|
||||
.fill('#B0B0B0')
|
||||
.fontSize(20)
|
||||
.drawText(0, 0, width + ' x ' + height, 'center');
|
||||
|
||||
res.set('Content-Type', 'image/png');
|
||||
magick.stream('png').pipe(res);
|
||||
};
|
||||
|
||||
let resizedImage = (req, res, { src, method, width, height }) => {
|
||||
let magick = gm(src);
|
||||
magick.format((err, format) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case 'resize':
|
||||
res.set('Content-Type', 'image/' + format.toLowerCase());
|
||||
magick.autoOrient()
|
||||
.resize(width, height)
|
||||
.stream()
|
||||
.pipe(res);
|
||||
return;
|
||||
|
||||
case 'cover':
|
||||
res.set('Content-Type', 'image/' + format.toLowerCase());
|
||||
magick.autoOrient()
|
||||
.resize(width, height + '^')
|
||||
.gravity('Center')
|
||||
.extent(width, height + '>')
|
||||
.stream()
|
||||
.pipe(res);
|
||||
return;
|
||||
|
||||
default:
|
||||
res.status(501).send(_('Method not supported'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// /editorapi/img?src=" + encodeURIComponent(src) + "&method=" + encodeURIComponent(method) + "¶ms=" + encodeURIComponent(width + "," + height);
|
||||
router.get('/img', passport.csrfProtection, (req, res) => {
|
||||
settings.get('serviceUrl', (err, serviceUrl) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
|
||||
let { src, method, params = '600,null' } = req.query;
|
||||
let width = params.split(',')[0];
|
||||
let height = params.split(',')[1];
|
||||
width = (width === 'null') ? null : Number(width);
|
||||
height = (height === 'null') ? null : Number(height);
|
||||
|
||||
switch (method) {
|
||||
case 'placeholder':
|
||||
return placeholderImage(req, res, { width, height });
|
||||
case 'resize':
|
||||
case 'cover':
|
||||
src = /^https?:\/\/|^\/\//i.test(src) ? src : url.resolve(serviceUrl, src);
|
||||
return resizedImage(req, res, { src, method, width, height });
|
||||
default:
|
||||
return res.status(501).send(_('Method not supported'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
prepareHtml({ editorName: req.query.editor, html: req.body.html }, (err, html) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
|
||||
req.body.html = html;
|
||||
|
||||
if (req.query.type === 'template') {
|
||||
templates.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
res.send('ok');
|
||||
});
|
||||
|
||||
} else if (req.query.type === 'campaign') {
|
||||
campaigns.update(req.body.id, req.body, (err, updated) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
res.send('ok');
|
||||
});
|
||||
|
||||
} else {
|
||||
res.status(500).send(_('Invalid resource type'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/artf/grapesjs/wiki/API-Asset-Manager
|
||||
// https://github.com/aguidrevitch/jquery-file-upload-middleware
|
||||
|
||||
router.get('/upload', passport.csrfProtection, (req, res) => {
|
||||
settings.get('serviceUrl', (err, serviceUrl) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
|
||||
let baseDir = path.join(__dirname, '..', 'public', req.query.editor, 'uploads');
|
||||
let baseDirUrl = serviceUrl + req.query.editor + '/uploads';
|
||||
|
||||
listImages(path.join(baseDir, '0'), baseDirUrl + '/0', (err, sharedImages) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
|
||||
if (req.query.type === 'campaign' && Number(req.query.id) > 0) {
|
||||
listImages(path.join(baseDir, req.query.id), baseDirUrl + '/' + req.query.id, (err, campaignImages) => {
|
||||
err ? res.status(500).send(err.message || err)
|
||||
: res.json({ files: sharedImages.concat(campaignImages) });
|
||||
});
|
||||
} else {
|
||||
res.json({ files: sharedImages });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/upload', passport.csrfProtection, (req, res) => {
|
||||
let dirName = req.query.type === 'template' ? '0'
|
||||
: req.query.type === 'campaign' && Number(req.query.id) > 0 ? req.query.id
|
||||
: null;
|
||||
|
||||
if (dirName === null) {
|
||||
return res.status(500).send(_('Invalid resource type or ID'));
|
||||
}
|
||||
|
||||
let opts = {
|
||||
tmpDir: config.www.tmpdir || os.tmpdir(),
|
||||
imageVersions: req.query.editor === 'mosaico' ? { thumbnail: { width: 90, height: 90 } } : {},
|
||||
uploadDir: path.join(__dirname, '..', 'public', req.query.editor, 'uploads', dirName),
|
||||
uploadUrl: '/' + req.query.editor + '/uploads/' + dirName, // must be root relative
|
||||
acceptFileTypes:/(\.|\/)(gif|jpe?g|png)$/i,
|
||||
};
|
||||
|
||||
let mockres = httpMocks.createResponse({
|
||||
eventEmitter: events.EventEmitter
|
||||
});
|
||||
|
||||
mockres.on('end', () => {
|
||||
if (req.query.editor === 'grapejs') {
|
||||
let data = [];
|
||||
JSON.parse(mockres._getData()).files.forEach(file => {
|
||||
data.push({ src: file.url });
|
||||
});
|
||||
res.json({ data });
|
||||
} else {
|
||||
res.send(mockres._getData());
|
||||
}
|
||||
});
|
||||
|
||||
jqueryFileUpload.fileHandler(opts)(req, mockres);
|
||||
});
|
||||
|
||||
router.post('/download', passport.csrfProtection, (req, res) => {
|
||||
prepareHtml({ editorName: req.query.editor, html: req.body.html }, (err, html) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
res.setHeader('Content-disposition', 'attachment; filename=' + req.body.filename);
|
||||
res.setHeader('Content-type', 'text/html');
|
||||
res.send(html);
|
||||
});
|
||||
});
|
||||
|
||||
let parseGrapejsMultipartTestForm = (req, res, next) => {
|
||||
if (req.query.editor === 'grapejs') {
|
||||
new multiparty.Form().parse(req, (err, fields, files) => {
|
||||
req.body.email = fields.email[0];
|
||||
req.body.subject = fields.subject[0];
|
||||
req.body.html = fields.html[0];
|
||||
req.body._csrf = fields._csrf[0];
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
router.post('/test', parseGrapejsMultipartTestForm, passport.csrfProtection, (req, res) => {
|
||||
prepareHtml({ editorName: req.query.editor, html: req.body.html }, (err, html) => {
|
||||
if (err) {
|
||||
req.query.editor === 'grapejs'
|
||||
? res.status(500).json({ errors: err.message || err })
|
||||
: res.status(500).send(err.message || err);
|
||||
return;
|
||||
}
|
||||
|
||||
settings.list(['defaultAddress', 'defaultFrom'], (err, configItems) => {
|
||||
if (err) {
|
||||
req.query.editor === 'grapejs'
|
||||
? res.status(500).json({ errors: err.message || err })
|
||||
: res.status(500).send(err.message || err);
|
||||
return;
|
||||
}
|
||||
|
||||
mailer.getMailer((err, transport) => {
|
||||
if (err) {
|
||||
req.query.editor === 'grapejs'
|
||||
? res.status(500).json({ errors: err.message || err })
|
||||
: res.status(500).send(err.message || err);
|
||||
return;
|
||||
}
|
||||
|
||||
let opts = {
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress,
|
||||
},
|
||||
to: req.body.email,
|
||||
subject: req.body.subject,
|
||||
text: htmlToText.fromString(html, { wordwrap: 100 }),
|
||||
html,
|
||||
};
|
||||
|
||||
transport.sendMail(opts, (err, info) => {
|
||||
if (err) {
|
||||
req.query.editor === 'grapejs'
|
||||
? res.status(500).json({ errors: err.message || err })
|
||||
: res.status(500).send(err.message || err);
|
||||
return;
|
||||
}
|
||||
|
||||
req.query.editor === 'grapejs'
|
||||
? res.json({ data: 'ok' })
|
||||
: res.send('ok');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/html-to-text', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
premailerApi.prepare({ html: req.body.html, fetchHTML: false }, (err, email) => {
|
||||
if (err) {
|
||||
return res.status(500).send(err.message || err);
|
||||
}
|
||||
res.send(email.text.replace(/%5B/g, '[').replace(/%5D/g, ']'));
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
50
routes/grapejs.js
Normal file
50
routes/grapejs.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let editorHelpers = require('../lib/editor-helpers.js')
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/editor', passport.csrfProtection, (req, res) => {
|
||||
editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
resource.editorName = resource.editorName || 'grapejs';
|
||||
resource.editorData = !resource.editorData
|
||||
? { template: req.query.template || 'demo' }
|
||||
: JSON.parse(resource.editorData);
|
||||
|
||||
if (!resource.html && !resource.editorData.html) {
|
||||
try {
|
||||
let file = path.join(__dirname, '..', 'public', 'grapejs', 'templates', resource.editorData.template, 'index.html');
|
||||
resource.html = fs.readFileSync(file, 'utf8');
|
||||
} catch (err) {
|
||||
resource.html = err.message || err;
|
||||
}
|
||||
}
|
||||
|
||||
res.render('grapejs/editor', {
|
||||
layout: 'grapejs/layout-editor',
|
||||
type: req.query.type,
|
||||
resource,
|
||||
editorConfig: config.grapejs,
|
||||
csrfToken: req.csrfToken(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
56
routes/mosaico.js
Normal file
56
routes/mosaico.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let express = require('express');
|
||||
let router = new express.Router();
|
||||
let passport = require('../lib/passport');
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let _ = require('../lib/translate')._;
|
||||
let editorHelpers = require('../lib/editor-helpers');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('danger', _('Need to be logged in to access restricted content'));
|
||||
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/editor', passport.csrfProtection, (req, res) => {
|
||||
editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
let getLanguageStrings = language => {
|
||||
if (!language || language === 'en') {
|
||||
return null;
|
||||
}
|
||||
language = language.split('_')[0];
|
||||
try {
|
||||
let file = path.join(__dirname, '..', 'public', 'mosaico', 'dist', 'lang', 'mosaico-' + language + '.json');
|
||||
return fs.readFileSync(file, 'utf8');
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
resource.editorName = resource.editorName || 'mosaico';
|
||||
resource.editorData = !resource.editorData
|
||||
? { template: req.query.template || 'versafix-1' }
|
||||
: JSON.parse(resource.editorData);
|
||||
|
||||
res.render('mosaico/editor', {
|
||||
layout: 'mosaico/layout-editor',
|
||||
type: req.query.type,
|
||||
resource,
|
||||
editorConfig: config.mosaico,
|
||||
languageStrings: getLanguageStrings(config.language),
|
||||
csrfToken: req.csrfToken(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
371
views/grapejs/editor.hbs
Normal file
371
views/grapejs/editor.hbs
Normal file
|
@ -0,0 +1,371 @@
|
|||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
iframe {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
#gjs-wrapper {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#toast-container {
|
||||
font-size: 13px;
|
||||
font-weight: lighter;
|
||||
}
|
||||
#toast-container > div,
|
||||
#toast-container > div:hover {
|
||||
box-shadow: 0 0 12px rgba(0, 0, 0, 0.1);
|
||||
font-family: Helvetica, sans-serif;
|
||||
}
|
||||
#toast-container > div {
|
||||
opacity: 0.95;
|
||||
}
|
||||
#merge-tag-reference-container {
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
#merge-tag-reference-container table th {
|
||||
padding: 5px 20px 5px 0;
|
||||
}
|
||||
|
||||
/* Fixed width sidebar */
|
||||
#gjs > .gjs-editor > .gjs-cv-canvas { width: 100% !important; padding-right: 300px !important; }
|
||||
#gjs-pn-views, #gjs-pn-views-container { width: 300px !important; }
|
||||
#gjs-pn-options { right: 300px !important; }
|
||||
#gjs-pn-commands { width: 100% !important; padding-right: 310px !important; }
|
||||
|
||||
/* Hide the fullscreen button - doesn't work from within our iFrame */
|
||||
#gjs-pn-options .gjs-pn-btn.fa.fa-arrows-alt { display: none !important; }
|
||||
</style>
|
||||
|
||||
|
||||
{{> editor_navbar}}
|
||||
|
||||
|
||||
<div id="gjs-wrapper">
|
||||
<div id="gjs" style="height:0px; overflow:hidden">
|
||||
{{#if resource.editorData.html}}
|
||||
<style>{{{resource.editorData.css}}}</style>
|
||||
{{{resource.editorData.html}}}
|
||||
{{else}}
|
||||
{{{resource.html}}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Test Newsletter Form -->
|
||||
|
||||
<form id="test-form" class="test-form" action="/editorapi/test?editor=grapejs" method="post" style="display: none">
|
||||
<div class="putsmail-c">
|
||||
<div class="gjs-sm-property" style="font-size: 10px">
|
||||
Hello! I'm a placerholder message.
|
||||
<span class="form-status" style="opacity: 0">
|
||||
<i class="fa fa-refresh anim-spin" aria-hidden="true"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gjs-sm-property">
|
||||
<div class="gjs-field">
|
||||
<span id="gjs-sm-input-holder">
|
||||
<input type="email" name="email" placeholder="Email" required>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gjs-sm-property">
|
||||
<div class="gjs-field">
|
||||
<span id="gjs-sm-input-holder">
|
||||
<input type="text" name="subject" placeholder="Subject" required value="Test {{resource.name}}">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="html">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<button class="gjs-btn-prim gjs-btn-import" style="width: 100%">SEND</button>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Merge Tag Reference -->
|
||||
|
||||
{{#if resource.mergeTags}}
|
||||
<div id="merge-tag-reference-container" style="display: none">
|
||||
<table class="">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Merge tag
|
||||
</th>
|
||||
<th>
|
||||
Description
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each resource.mergeTags}}
|
||||
<tr>
|
||||
<th>
|
||||
[{{key}}]
|
||||
</th>
|
||||
<td>
|
||||
{{value}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
<script>
|
||||
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
|
||||
|
||||
var editor = grapesjs.init({
|
||||
height: '100%',
|
||||
storageManager: {
|
||||
type: 'none'
|
||||
},
|
||||
assetManager: {
|
||||
assets: [],
|
||||
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}',
|
||||
uploadText: 'Drop images here or click to upload',
|
||||
},
|
||||
container : '#gjs',
|
||||
fromElement: true,
|
||||
plugins: ['gjs-preset-newsletter'],
|
||||
pluginsOpts: {
|
||||
'gjs-preset-newsletter': {
|
||||
modalLabelImport: 'Paste all your code here below and click import',
|
||||
modalLabelExport: 'Copy the code and use it wherever you want',
|
||||
codeViewerTheme: 'material',
|
||||
importPlaceholder: '<table class="table"><tr><td class="cell">Hello world!</td></tr></table>',
|
||||
cellStyle: {
|
||||
'font-size': '12px',
|
||||
'font-weight': 300,
|
||||
'vertical-align': 'top',
|
||||
color: 'rgb(111, 119, 125)',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$.getJSON('/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}', function(data) {
|
||||
editor.AssetManager.add(data.files);
|
||||
});
|
||||
|
||||
function getPreparedHtml() {
|
||||
var imgs = [];
|
||||
$('.gjs-pn-buttons > .gjs-pn-btn.fa.fa-desktop').click();
|
||||
$('.gjs-editor > .gjs-cv-canvas > iframe.gjs-frame')
|
||||
.contents()
|
||||
.find('img')
|
||||
.each(function() {
|
||||
var src = $(this).attr('src');
|
||||
var s = src.match(/\/editorapi\/img\?src=([^&]*)/);
|
||||
var encodedSrc = (s && s[1]) || encodeURIComponent(src);
|
||||
var dynamicSrc = '/editorapi/img?src=' + encodedSrc + '&method=resize¶ms=' + $(this).width() + '%2C' + $(this).height();
|
||||
imgs.push({
|
||||
cls: $(this).attr('class'),
|
||||
dynamicSrc: dynamicSrc,
|
||||
src: src,
|
||||
});
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
var mdlClass = 'gjs-mdl-dialog-sm';
|
||||
var pnm = editor.Panels;
|
||||
var cmdm = editor.Commands;
|
||||
var testContainer = document.getElementById("test-form");
|
||||
var contentEl = testContainer.querySelector('input[name=html]');
|
||||
var md = editor.Modal;
|
||||
|
||||
cmdm.add('send-test', {
|
||||
run(editor, sender) {
|
||||
sender.set('active', 0);
|
||||
var modalContent = md.getContentEl();
|
||||
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
|
||||
// var cmdGetCode = cmdm.get('gjs-get-inlined-html');
|
||||
// contentEl.value = cmdGetCode && cmdGetCode.run(editor);
|
||||
contentEl.value = getPreparedHtml();
|
||||
mdlDialog.className += ' ' + mdlClass;
|
||||
testContainer.style.display = 'block';
|
||||
md.setTitle('Test your Newsletter');
|
||||
md.setContent(testContainer);
|
||||
md.open();
|
||||
md.getModel().once('change:open', function() {
|
||||
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
|
||||
//clean status
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
pnm.addButton('options', {
|
||||
id: 'send-test',
|
||||
className: 'fa fa-paper-plane',
|
||||
command: 'send-test',
|
||||
attributes: {
|
||||
'title': 'Test Newsletter',
|
||||
'data-tooltip-pos': 'bottom',
|
||||
},
|
||||
});
|
||||
|
||||
var statusFormElC = document.querySelector('.form-status');
|
||||
var statusFormEl = document.querySelector('.form-status i');
|
||||
var ajaxTest = ajaxable(testContainer, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
|
||||
.onStart(function() {
|
||||
statusFormEl.className = 'fa fa-refresh anim-spin';
|
||||
statusFormElC.style.opacity = '1';
|
||||
statusFormElC.className = 'form-status';
|
||||
})
|
||||
.onResponse(function(res) {
|
||||
if (res.data) {
|
||||
statusFormElC.style.opacity = '0';
|
||||
statusFormEl.removeAttribute('data-tooltip');
|
||||
md.close();
|
||||
} else if (res.errors) {
|
||||
statusFormEl.className = 'fa fa-exclamation-circle';
|
||||
statusFormEl.setAttribute('data-tooltip', res.errors);
|
||||
statusFormElC.className = 'form-status text-danger';
|
||||
}
|
||||
});
|
||||
|
||||
cmdm.add('tlb-edit', {
|
||||
run: function(ed) {
|
||||
alert('No image editor installed');
|
||||
},
|
||||
});
|
||||
|
||||
// Add Merge Tag Reference command
|
||||
var mergeTagReferenceContainer = document.getElementById('merge-tag-reference-container');
|
||||
cmdm.add('open-merge-tag-reference', {
|
||||
run(editor, sender) {
|
||||
sender.set('active', 0);
|
||||
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
|
||||
mdlDialog.className += ' gjs-mdl-dialog-lg';
|
||||
mergeTagReferenceContainer.style.display = 'block';
|
||||
md.setTitle('Merge tag reference');
|
||||
md.setContent(mergeTagReferenceContainer);
|
||||
md.open();
|
||||
md.getModel().once('change:open', function() {
|
||||
mdlDialog.className = mdlDialog.className.replace('gjs-mdl-dialog-lg', '');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
pnm.addButton('options', {
|
||||
id: 'view-merge-tag-reference',
|
||||
className: 'fa fa-tags',
|
||||
command: 'open-merge-tag-reference',
|
||||
attributes: {
|
||||
'title': 'Merge tag reference',
|
||||
'data-tooltip-pos': 'bottom',
|
||||
},
|
||||
});
|
||||
|
||||
// Simple warn notifier
|
||||
var origWarn = console.warn;
|
||||
toastr.options = {
|
||||
closeButton: true,
|
||||
preventDuplicates: true,
|
||||
showDuration: 250,
|
||||
hideDuration: 150
|
||||
};
|
||||
console.warn = function(msg) {
|
||||
toastr.warning(msg);
|
||||
origWarn(msg);
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
// Beautify tooltips
|
||||
$('*[title]').each(function() {
|
||||
var el = $(this);
|
||||
var title = el.attr('title').trim();
|
||||
if (title) {
|
||||
el.attr('data-tooltip', 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;
|
||||
})
|
||||
.fail(function(data) {
|
||||
alert(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>
|
23
views/grapejs/layout-editor.hbs
Normal file
23
views/grapejs/layout-editor.hbs
Normal file
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>GrapesJS Newsletter Editor</title>
|
||||
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.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/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="/grapejs/dist/js/grapes.min.js"></script>
|
||||
<script src="/grapejs/dist/js/grapesjs-preset-newsletter.min.js"></script>
|
||||
<script src="/grapejs/dist/js/toastr.min.js"></script>
|
||||
<script src="/grapejs/dist/js/ajaxable.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{{{body}}}
|
||||
|
||||
</body>
|
||||
</html>
|
286
views/mosaico/editor.hbs
Normal file
286
views/mosaico/editor.hbs
Normal file
|
@ -0,0 +1,286 @@
|
|||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#mt-navbar {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: -34px;
|
||||
}
|
||||
#mt-navbar .mt-logo {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#merge-tag-reference-container * {
|
||||
background: none;
|
||||
text-align: left;
|
||||
border: none;
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
#merge-tag-reference-container th,
|
||||
#merge-tag-reference-container td {
|
||||
padding: 5px 10px 5px 0;
|
||||
height: 2em;
|
||||
}
|
||||
@media screen and (max-width: 1175px) {
|
||||
a.ui-button[title='Download template'] {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1020px) {
|
||||
#btnMergeReference {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
{{> editor_navbar}}
|
||||
|
||||
|
||||
{{#if resource.mergeTags}}
|
||||
<div id="merge-tag-reference-container" title="{{#translate}}Merge tag reference{{/translate}}" style="display: none">
|
||||
<table class="">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{{#translate}}Merge tag{{/translate}}
|
||||
</th>
|
||||
<th>
|
||||
{{#translate}}Description{{/translate}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each resource.mergeTags}}
|
||||
<tr>
|
||||
<th>
|
||||
[{{key}}]
|
||||
</th>
|
||||
<td>
|
||||
{{value}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
|
||||
if (!Mosaico.isCompatible()) {
|
||||
alert('Update your browser!');
|
||||
return;
|
||||
}
|
||||
|
||||
window.bridge = window.bridge || {};
|
||||
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
|
||||
|
||||
var uploadHost = window.location.protocol + '//' + window.location.host;
|
||||
var plugins = [];
|
||||
var config = {
|
||||
// Mosaico needs absolute URLs
|
||||
imgProcessorBackend: uploadHost + '/editorapi/img',
|
||||
titleToken: '{{#translate}}MOSAICO Responsive Email Designer{{/translate}}',
|
||||
fileuploadConfig: {
|
||||
url: uploadHost + '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{resource.editorName}}',
|
||||
},
|
||||
{{#if languageStrings}}
|
||||
strings: {{{languageStrings}}},
|
||||
{{/if}}
|
||||
};
|
||||
|
||||
|
||||
// Save Button
|
||||
|
||||
plugins.push(function(viewModel) {
|
||||
$(document).ready(function() {
|
||||
$('#mt-save').on('click', function() {
|
||||
if ($(this).hasClass('busy')) {
|
||||
return;
|
||||
}
|
||||
$(this).addClass('busy');
|
||||
|
||||
var html = viewModel.exportHTML();
|
||||
|
||||
$.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}}',
|
||||
model: viewModel.exportJSON(),
|
||||
metadata: viewModel.exportMetadata(),
|
||||
}),
|
||||
}, null, 'html')
|
||||
.success(function() {
|
||||
setTimeout(function() {
|
||||
window.bridge.lastSavedHtml = html;
|
||||
viewModel.notifier.success('{{#translate}}Sucessfully saved{{/translate}}');
|
||||
$(this).removeClass('busy');
|
||||
}.bind(this), 500); // Don't save too fast
|
||||
}.bind(this))
|
||||
.fail(function(data) {
|
||||
viewModel.notifier.error(data.responseText || '{{#translate}}An error occured while saving the document{{/translate}}');
|
||||
$(this).removeClass('busy');
|
||||
}.bind(this))
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Close Button
|
||||
|
||||
plugins.push(function(viewModel) {
|
||||
$(document).ready(function() {
|
||||
$('#mt-close').on('click', function() {
|
||||
if (confirm('{{#translate}}Unsaved changes will be lost. Close now?{{/translate}}') === true) {
|
||||
window.bridge.exit
|
||||
? window.bridge.exit()
|
||||
: window.location.href = '/{{type}}s/edit/{{resource.id}}?tab=template';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Download Button
|
||||
|
||||
plugins.push(function(viewModel) {
|
||||
viewModel.download = {
|
||||
name: 'Download', // Translated by Mosaico
|
||||
enabled: ko.observable(true),
|
||||
execute: function() {
|
||||
viewModel.download.enabled(false);
|
||||
viewModel.notifier.info(viewModel.t('Downloading...'));
|
||||
viewModel.exportHTMLtoTextarea('#downloadHtmlTextarea');
|
||||
var $downloadForm = $('#downloadForm');
|
||||
$downloadForm.attr('action', '/editorapi/download?editor={{resource.editorName}}');
|
||||
if (!$downloadForm.find('input[name=_csrf]').length) {
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: '_csrf',
|
||||
value: '{{csrfToken}}',
|
||||
}).appendTo($downloadForm);
|
||||
}
|
||||
$downloadForm.submit();
|
||||
viewModel.download.enabled(true);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Test E-Mail Button
|
||||
|
||||
plugins.push(function(viewModel) {
|
||||
viewModel.test = {
|
||||
name: 'Test', // Translated by Mosaico
|
||||
enabled: ko.observable(true),
|
||||
isValidEmail: function(email) {
|
||||
return /\S+@\S+\.\S+/.test(email);
|
||||
},
|
||||
execute: function() {
|
||||
viewModel.test.enabled(false);
|
||||
var email = localStorage.getItem('testemail');
|
||||
if (!viewModel.test.isValidEmail(email)) {
|
||||
email = viewModel.t('Insert here the recipient email address');
|
||||
}
|
||||
email = prompt(viewModel.t('Test email address'), email);
|
||||
if (viewModel.test.isValidEmail(email)) {
|
||||
localStorage.setItem('testemail', email);
|
||||
$.post('/editorapi/test?editor={{resource.editorName}}', {
|
||||
email: email,
|
||||
subject: '[test] {{resource.name}}',
|
||||
html: viewModel.exportHTML(),
|
||||
}, null, 'html')
|
||||
.success(function() {
|
||||
viewModel.notifier.success(viewModel.t('Test email sent...'));
|
||||
})
|
||||
.fail(function() {
|
||||
viewModel.notifier.error(viewModel.t('Unexpected error talking to server: contact us!'));
|
||||
})
|
||||
.always(function() {
|
||||
viewModel.test.enabled(true);
|
||||
});
|
||||
} else {
|
||||
viewModel.test.enabled(true);
|
||||
email !== null && alert(viewModel.t('Invalid email address'));
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Merge Tag Reference
|
||||
|
||||
plugins.push(function(viewModel) {
|
||||
var addButton = function() {
|
||||
var $toobarRightButtons = $('#toolbar .rightButtons');
|
||||
if (!$toobarRightButtons.length) {
|
||||
setTimeout(addButton, 50);
|
||||
return;
|
||||
}
|
||||
$('<a title="{{#translate}}Tags{{/translate}}" id="btnMergeReference" class="ui-button ui-corner-all ui-widget" role="button">\
|
||||
<span class="ui-button-icon ui-icon fa fa-fw fa-tags"></span>\
|
||||
<span class="ui-button-icon-space"></span>{{#translate}}Tags{{/translate}}\
|
||||
</a>')
|
||||
.prependTo($toobarRightButtons)
|
||||
.css('color', 'white', '!important')
|
||||
.on('click', function() {
|
||||
$('#merge-tag-reference-container').dialog({
|
||||
modal: true,
|
||||
width: 'auto',
|
||||
draggable: true,
|
||||
resizable: false,
|
||||
appendTo: '#mo-body',
|
||||
closeText: '',
|
||||
});
|
||||
});
|
||||
};
|
||||
addButton();
|
||||
});
|
||||
|
||||
|
||||
// (Custom) HTML postRenderers
|
||||
|
||||
plugins.push(function(viewModel) {
|
||||
viewModel.originalExportHTML = viewModel.exportHTML;
|
||||
viewModel.exportHTML = function() {
|
||||
var html = viewModel.originalExportHTML();
|
||||
window.mosaicoHTMLPostRenderers.forEach(function(postRender) {
|
||||
html = postRender(html);
|
||||
});
|
||||
return html;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// Finally start Mosaico
|
||||
|
||||
Mosaico.start(
|
||||
config,
|
||||
uploadHost + '/{{resource.editorName}}/templates/{{resource.editorData.template}}/index.html',
|
||||
{{#if resource.editorData.metadata}} {{{resource.editorData.metadata}}} {{else}} undefined {{/if}},
|
||||
{{#if resource.editorData.model}} {{{resource.editorData.model}}} {{else}} undefined {{/if}},
|
||||
plugins.concat(window.mosaicoPlugins)
|
||||
);
|
||||
|
||||
});
|
||||
</script>
|
40
views/mosaico/layout-editor.hbs
Normal file
40
views/mosaico/layout-editor.hbs
Normal file
|
@ -0,0 +1,40 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=1024, initial-scale=1">
|
||||
|
||||
<script src="dist/vendor/jquery.min.js"></script>
|
||||
<script src="dist/vendor/knockout.js"></script>
|
||||
<script src="dist/vendor/jquery-ui.min.js"></script>
|
||||
<script src="dist/vendor/jquery.ui.touch-punch.min.js"></script>
|
||||
<script src="dist/vendor/load-image.all.min.js"></script>
|
||||
<script src="dist/vendor/canvas-to-blob.min.js"></script>
|
||||
<script src="dist/vendor/jquery.iframe-transport.js"></script>
|
||||
<script src="dist/vendor/jquery.fileupload.js"></script>
|
||||
<script src="dist/vendor/jquery.fileupload-process.js"></script>
|
||||
<script src="dist/vendor/jquery.fileupload-image.js"></script>
|
||||
<script src="dist/vendor/jquery.fileupload-validate.js"></script>
|
||||
<script src="dist/vendor/knockout-jqueryui.min.js"></script>
|
||||
<script src="dist/vendor/tinymce.min.js"></script>
|
||||
|
||||
<script src="dist/mosaico.min.js?v=0.16"></script>
|
||||
|
||||
<script> window.mosaicoPlugins = []; </script>
|
||||
<script> window.mosaicoHTMLPostRenderers = []; </script>
|
||||
|
||||
{{#if editorConfig.customscripts}}
|
||||
{{#each editorConfig.customscripts}}
|
||||
<script src="custom/{{this}}"></script>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
<link rel="stylesheet" href="dist/mosaico-material.min.css?v=0.10" />
|
||||
<link rel="stylesheet" href="dist/vendor/notoregular/stylesheet.css" />
|
||||
</head>
|
||||
<body class="mo-standalone">
|
||||
|
||||
{{{body}}}
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -3,36 +3,45 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
#editor-frame,
|
||||
#editor-frame-spinner {
|
||||
border: none;
|
||||
#editor-frame-loader {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
#editor-frame-spinner {
|
||||
#editor-frame-loader {
|
||||
z-index: 10001;
|
||||
background: white;
|
||||
background: #eaeced;
|
||||
}
|
||||
#editor-frame-spinner div {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-top: -60px;
|
||||
margin-left: -60px;
|
||||
#editor-frame-loader div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 62px; margin-left: -31px;
|
||||
height: 72px; margin-top: -36px;
|
||||
background: url('/mailtrain-header.png');
|
||||
-webkit-animation: pulsate 1.2s ease-out;
|
||||
animation: pulsate 1.2s ease-out;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
#editor-frame-spinner span:before {
|
||||
font-size: 120px;
|
||||
color: #efefef;
|
||||
@-webkit-keyframes pulsate {
|
||||
0% { -webkit-transform: scale(0.1, 0.1); opacity: 0.5; }
|
||||
70% { opacity: 1.0; }
|
||||
100% { -webkit-transform: scale(1.2, 1.2); opacity: 0.0; }
|
||||
}
|
||||
@keyframes pulsate {
|
||||
0% { transform: scale(0.1, 0.1); opacity: 0.5; }
|
||||
70% { opacity: 1.0; }
|
||||
100% { transform: scale(1.2, 1.2); opacity: 0.0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="editor-frame-spinner" style="display: none;">
|
||||
<div><span class="glyphicon glyphicon-refresh spinning"></span></div>
|
||||
<div id="editor-frame-loader" style="display: none;">
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
@ -51,13 +60,13 @@
|
|||
closeEditor();
|
||||
}
|
||||
setTimeout(function() {
|
||||
$('#editor-frame-spinner').hide();
|
||||
}, 200);
|
||||
$('#editor-frame-loader').hide();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
var openEditor = function() {
|
||||
$('body').addClass('noscroll');
|
||||
$('#editor-frame-spinner').show();
|
||||
$('#editor-frame-loader').show();
|
||||
$editorFrame.appendTo($('body'));
|
||||
}
|
||||
|
||||
|
@ -67,7 +76,7 @@
|
|||
if (editorWindow.bridge.lastSavedHtml) {
|
||||
$('#template-html').val(editorWindow.bridge.lastSavedHtml);
|
||||
$('#html-preview-frame').attr('srcdoc', editorWindow.bridge.lastSavedHtml);
|
||||
$('#html-preview').show(); // it's hidden when there's no html
|
||||
$('#html-preview').show();
|
||||
}
|
||||
// Reload to discard unsaved changes
|
||||
$editorFrame.attr('src', '/{{editorName}}/editor?id={{id}}&type=' + type + '&cb=' + Math.random());
|
||||
|
|
44
views/partials/editor-navbar.hbs
Normal file
44
views/partials/editor-navbar.hbs
Normal file
|
@ -0,0 +1,44 @@
|
|||
<style>
|
||||
#mt-navbar {
|
||||
background: #DE4320;
|
||||
overflow: hidden;
|
||||
height: 34px;
|
||||
}
|
||||
.mt-logo {
|
||||
height: 24px;
|
||||
padding: 5px 0 5px 35px;
|
||||
-webkit-filter: brightness(0) invert(1);
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
.mt-btn {
|
||||
display: block;
|
||||
float: right;
|
||||
width: 150px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
font-family: sans-serif;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #972E15;
|
||||
}
|
||||
.mt-btn:hover {
|
||||
background: #972E15;
|
||||
}
|
||||
#mt-save span:after {
|
||||
content: '{{#translate}}SAVE{{/translate}}';
|
||||
}
|
||||
#mt-save.busy span:after {
|
||||
content: '{{#translate}}SAVING{{/translate}}';
|
||||
}
|
||||
#mt-save.busy {
|
||||
background: #972E15;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="mt-navbar">
|
||||
<img class="mt-logo" src="/mailtrain-header.png">
|
||||
<a id="mt-close" class="mt-btn">{{#translate}}CLOSE{{/translate}}</a>
|
||||
<a id="mt-save" class="mt-btn"><span></span></a>
|
||||
</div>
|
27
views/partials/grapejs.hbs
Normal file
27
views/partials/grapejs.hbs
Normal file
|
@ -0,0 +1,27 @@
|
|||
<style>
|
||||
#btn-open-grapejs {
|
||||
background-color: #4E6A8A;
|
||||
border-color: #4E6A8A;
|
||||
padding-left: 36px;
|
||||
padding-right: 36px;
|
||||
}
|
||||
#btn-open-grapejs:hover {
|
||||
background-color: #394C61;
|
||||
border-color: #394C61;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{> html_to_text}}
|
||||
|
||||
<input type="hidden" id="template-html" value="{{html}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{{#translate}}Template content (HTML){{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<a class="btn btn-lg btn-success" role="button" id="btn-open-grapejs">{{#translate}}Open GrapeJS{{/translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{> html_preview}}
|
||||
|
||||
{{> editor_bridge}}
|
41
views/partials/html-to-text.hbs
Normal file
41
views/partials/html-to-text.hbs
Normal file
|
@ -0,0 +1,41 @@
|
|||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<span class="help-block">
|
||||
{{#translate}}To extract the text from HTML click <a id="html-to-plaintext-btn" role="button">here</a>.{{/translate}}
|
||||
<span id="html-to-plaintext-spinner" class="glyphicon glyphicon-refresh spinning hidden"></span>
|
||||
{{#translate}}Please note that your existing plaintext in the field above will be overwritten. This feature uses the <a href="http://premailer.dialect.ca/api" target="_blank" rel="noreferrer">Premailer API</a>, a third party service. Their Terms of Service and Privacy Policy apply.{{/translate}}
|
||||
<span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } });
|
||||
var $spinner = $('#html-to-plaintext-spinner');
|
||||
|
||||
$('#html-to-plaintext-btn').on('click', function() {
|
||||
var html = $('#template-html').val();
|
||||
|
||||
if (!html) {
|
||||
alert('Missing HTML content');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$spinner.hasClass('hidden')) {
|
||||
return;
|
||||
}
|
||||
$spinner.removeClass('hidden');
|
||||
|
||||
$.post('/editorapi/html-to-text?editor={{editorName}}', { html: html }, null, 'html')
|
||||
.success(function(data) {
|
||||
$('#template-text').val(data);
|
||||
})
|
||||
.fail(function(data) {
|
||||
alert(data.responseText || '{{#translate}}An error occurred while talking to the server{{/translate}}');
|
||||
})
|
||||
.always(function() {
|
||||
$spinner.addClass('hidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
27
views/partials/mosaico.hbs
Normal file
27
views/partials/mosaico.hbs
Normal file
|
@ -0,0 +1,27 @@
|
|||
<style>
|
||||
#btn-open-mosaico {
|
||||
background-color: #4E6A8A;
|
||||
border-color: #4E6A8A;
|
||||
padding-left: 36px;
|
||||
padding-right: 36px;
|
||||
}
|
||||
#btn-open-mosaico:hover {
|
||||
background-color: #394C61;
|
||||
border-color: #394C61;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{> html_to_text}}
|
||||
|
||||
<input type="hidden" id="template-html" value="{{html}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">{{#translate}}Template content (HTML){{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<a class="btn btn-lg btn-success" role="button" id="btn-open-mosaico">{{#translate}}Open Mosaico{{/translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{> html_preview}}
|
||||
|
||||
{{> editor_bridge}}
|
Loading…
Add table
Add a link
Reference in a new issue