More elements for mosaico mjml support. Added "MJML Sample" wizard to mosaico templates.

This commit is contained in:
Tomas Bures 2019-04-03 23:39:10 +02:00
parent ec0f288d81
commit 94a2cdf89e
7 changed files with 415 additions and 12 deletions

View file

@ -27,7 +27,7 @@ import {
} from '../../lib/namespace';
import {DeleteModalDialog} from "../../lib/modals";
import {getVersafix} from "../../../../shared/mosaico-templates";
import {getVersafix, getMJMLSample} from "../../../../shared/mosaico-templates";
import {
getTemplateTypes,
getTemplateTypesOrder
@ -87,6 +87,15 @@ export default class CUD extends Component {
html: getVersafix()
});
} else if (wizard === 'mjml-sample') {
this.populateFormValues({
name: '',
description: '',
namespace: mailtrainConfig.user.namespace,
type: 'mjml',
mjml: getMJMLSample()
});
} else {
this.populateFormValues({
name: '',
@ -183,7 +192,7 @@ export default class CUD extends Component {
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('name')}/>
<TextArea id="description" label={t('description')}/>
{isEdit ?
{isEdit || this.props.wizard ?
<StaticField id="type" className={styles.formDisabled} label={t('type')}>
{typeKey && this.templateTypes[typeKey].typeName}
</StaticField>

View file

@ -123,6 +123,7 @@ export default class List extends Component {
<ButtonDropdown buttonClassName="btn-primary" menuClassName="dropdown-menu-right" label={t('createMosaicoTemplate')}>
<DropdownLink to="/templates/mosaico/create">{t('blank')}</DropdownLink>
<DropdownLink to="/templates/mosaico/create/versafix">{t('versafixOne')}</DropdownLink>
<DropdownLink to="/templates/mosaico/create/mjml-sample">{t('MJML Sample')}</DropdownLink>
</ButtonDropdown>
</Toolbar>
}

View file

@ -52,6 +52,7 @@ export function getTemplateTypes(t) {
owner.setFormStatusMessage('success', t('MJML is valid.'));
}
} catch (err) {
console.log(err);
owner.setFormStatusMessage('danger', t('Invalid MJML.'));
}
}

View file

@ -45,7 +45,7 @@ function handleMosaicoEditable(block, src) {
for (const attrMatch of attrsMatches) {
if (attrMatch[1] === 'mosaico-editable') {
if (attrMatch[1] === 'mj-mosaico-editable') {
const propertyId = attrMatch[2];
block.addMosaicoProperty(propertyId);
@ -80,13 +80,24 @@ class MjMosaicoProperty extends HeadComponent {
static allowedAttributes = {
'property-id': 'string',
label: 'string',
widget: 'string',
type: 'enum(image,link)'
}
handler() {
const { add } = this.context;
add('style', ` @supports -ko-blockdefs { ${this.getAttribute('property-id')} { label: ${this.getAttribute('label')}; widget: ${this.getAttribute('widget')} } }`);
let properties = null;
const type = this.getAttribute('type');
if (type === 'image') {
properties = 'src url alt';
} else if (type === 'link') {
properties = 'text url';
}
const propertiesStr = properties ? ` properties: ${properties}` : '';
add('style', ` @supports -ko-blockdefs { ${this.getAttribute('property-id')} { label: ${this.getAttribute('label')};${propertiesStr} } }`);
}
}
@ -247,7 +258,10 @@ class MjMosaicoImage extends BodyComponent {
const propertyId = this.getAttribute('property-id');
this.propertyId = propertyId || `image_${getId()}`;
getParent().addMosaicoProperty(this.propertyId);
const parentBlock = getParent();
if (parentBlock) {
parentBlock.addMosaicoProperty(this.propertyId);
}
}
static tagOmission = true
@ -425,6 +439,154 @@ class MjMosaicoImage extends BodyComponent {
}
// Adapted from https://github.com/mjmlio/mjml/blob/master/packages/mjml-button/src/index.js
class MjMosaicoButton extends BodyComponent {
constructor(initialDatas = {}) {
super(initialDatas);
const propertyId = this.getAttribute('property-id');
this.propertyId = propertyId || `button_${getId()}`;
const parentBlock = getParent();
if (parentBlock) {
parentBlock.addMosaicoProperty(this.propertyId);
}
}
static endingTag = true
static allowedAttributes = {
'property-id': 'string',
align: 'enum(left,center,right)',
'background-color': 'color',
'border-bottom': 'string',
'border-left': 'string',
'border-radius': 'string',
'border-right': 'string',
'border-top': 'string',
border: 'string',
color: 'color',
'container-background-color': 'color',
'font-family': 'string',
'font-size': 'unit(px)',
'font-style': 'string',
'font-weight': 'string',
height: 'unit(px,%)',
name: 'string',
'inner-padding': 'unit(px,%)',
'line-height': 'unit(px,%)',
'padding-bottom': 'unit(px,%)',
'padding-left': 'unit(px,%)',
'padding-right': 'unit(px,%)',
'padding-top': 'unit(px,%)',
padding: 'unit(px,%){1,4}',
rel: 'string',
target: 'string',
'text-decoration': 'string',
'text-transform': 'string',
'vertical-align': 'enum(top,bottom,middle)',
'text-align': 'enum(left,right,center)',
width: 'unit(px,%)',
}
static defaultAttributes = {
align: 'center',
'background-color': '#414141',
border: 'none',
'border-radius': '3px',
color: '#ffffff',
'font-family': 'Ubuntu, Helvetica, Arial, sans-serif',
'font-size': '13px',
'font-weight': 'normal',
'inner-padding': '10px 25px',
'line-height': '120%',
padding: '10px 25px',
target: '_blank',
'text-decoration': 'none',
'text-transform': 'none',
'vertical-align': 'middle',
}
getStyles() {
return {
table: {
'border-collapse': 'separate',
width: this.getAttribute('width'),
'line-height': '100%',
},
td: {
border: this.getAttribute('border'),
'border-bottom': this.getAttribute('border-bottom'),
'border-left': this.getAttribute('border-left'),
'border-radius': this.getAttribute('border-radius'),
'border-right': this.getAttribute('border-right'),
'border-top': this.getAttribute('border-top'),
cursor: 'auto',
'font-style': this.getAttribute('font-style'),
height: this.getAttribute('height'),
padding: this.getAttribute('inner-padding'),
'text-align': this.getAttribute('text-align'),
background: this.getAttribute('background-color'),
},
content: {
background: this.getAttribute('background-color'),
color: this.getAttribute('color'),
'font-family': this.getAttribute('font-family'),
'font-size': this.getAttribute('font-size'),
'font-style': this.getAttribute('font-style'),
'font-weight': this.getAttribute('font-weight'),
'line-height': this.getAttribute('line-height'),
Margin: '0',
'text-decoration': this.getAttribute('text-decoration'),
'text-transform': this.getAttribute('text-transform'),
},
}
}
render() {
return `
<table
${this.htmlAttributes({
border: '0',
cellpadding: '0',
cellspacing: '0',
role: 'presentation',
style: 'table',
})}
>
<tr>
<td
${this.htmlAttributes({
align: 'center',
bgcolor:
this.getAttribute('background-color') === 'none'
? undefined
: this.getAttribute('background-color'),
role: 'presentation',
style: 'td',
valign: this.getAttribute('vertical-align'),
})}
>
<a
${this.htmlAttributes({
rel: this.getAttribute('rel'),
name: this.getAttribute('name'),
style: 'content',
target: this.getAttribute('target'),
'data-ko-editable': this.getAttribute('property-id') + '.text',
'data-ko-link': this.getAttribute('property-id') + '.url'
})}
>
${this.getContent()}
</a>
</td>
</tr>
</table>
`;
}
}
const mjmlInstance = new MJML();
@ -432,15 +594,21 @@ mjmlInstance.registerComponent(MjMosaicoContainer);
mjmlInstance.registerComponent(MjMosaicoBlock);
mjmlInstance.registerComponent(MjMosaicoInnerBlock);
mjmlInstance.registerComponent(MjMosaicoImage);
mjmlInstance.registerComponent(MjMosaicoButton);
mjmlInstance.registerComponent(MjMosaicoProperty);
mjmlInstance.registerDependencies({
'mj-mosaico-container': ['mj-mosaico-block', 'mj-mosaico-inner-block'],
'mj-body': ['mj-mosaico-container', 'mj-mosaico-block'],
'mj-section': ['mj-mosaico-container', 'mj-mosaico-block'],
'mj-column': ['mj-mosaico-container', 'mj-mosaico-inner-block', 'mj-mosaico-image'],
'mj-column': ['mj-mosaico-container', 'mj-mosaico-inner-block', 'mj-mosaico-image', 'mj-mosaico-button'],
'mj-mosaico-block': ['mj-section', 'mj-column'],
'mj-mosaico-inner-block': ['mj-mosaico-image', 'mj-divider', 'mj-text', 'mj-image', 'mj-table']
'mj-mosaico-inner-block': [
'mj-mosaico-image', 'mj-mosaico-button',
'mj-accordion', 'mj-button', 'mj-carousel', 'mj-divider', 'mj-html', 'mj-image', 'mj-invoice', 'mj-list',
'mj-location', 'mj-raw', 'mj-social', 'mj-spacer', 'mj-table', 'mj-text', 'mj-navbar'
],
'mj-head': ['mj-mosaico-property']
});
mjmlInstance.addToHeader(`

View file

@ -47,6 +47,7 @@
"bluebird": "^3.5.3",
"body-parser": "^1.18.3",
"bounce-handler": "7.3.2-fork.3",
"capitalize": "^2.0.0",
"compression": "^1.7.3",
"config": "^3.0.1",
"connect-flash": "^0.1.1",

View file

@ -10,6 +10,7 @@ const gm = require('gm').subClass({
imageMagick: true
});
const users = require('../models/users');
const capitalize = require('capitalize');
const fs = require('fs-extra')
@ -51,7 +52,7 @@ users.registerRestrictedAccessTokenMethod('mosaico', async ({entityTypeId, entit
});
async function placeholderImage(width, height) {
async function placeholderImage(width, height, labelText, labelColor) {
const magick = gm(width, height, '#707070');
const streamAsync = bluebird.promisify(magick.stream.bind(magick));
@ -72,11 +73,14 @@ async function placeholderImage(width, height) {
}
}
labelText = labelText || `${width} x ${height}`;
labelColor = labelColor || '#B0B0B0';
// text
magick
.fill('#B0B0B0')
.fill(labelColor)
.fontSize(20)
.drawText(0, 0, width + ' x ' + height, 'center');
.drawText(0, 0, labelText, 'center');
const stream = await streamAsync('png');
@ -156,6 +160,17 @@ function getRouter(appType) {
// This is a fallback to versafix-1 if the block thumbnail is not defined by the template
router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', '..', 'client', 'static', 'mosaico', 'templates', 'versafix-1', 'edres')));
// This is the final fallback for a block thumbnail, so that at least something gets returned
router.getAsync('/templates/:mosaicoTemplateId/edres/:fileName', async (req, res, next) => {
let labelText = req.params.fileName.replace(/\.png$/, '');
labelText = labelText.replace(/[_]/g, ' ');
labelText = capitalize.words(labelText);
const image = await placeholderImage(340, 100, labelText, '#ffffff');
res.set('Content-Type', 'image/' + image.format);
image.stream.pipe(res);
});
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file', resp => {
return {
files: resp.files.map(f => ({name: f.name, url: f.url, size: f.size, thumbnailUrl: f.thumbnailUrl}))

View file

@ -1532,10 +1532,218 @@ const versafix = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
'</body>\n' +
'</html>\n';
const mjmlSample = '<mjml>\n' +
' <mj-head>\n' +
' <mj-attributes>\n' +
' <mj-body background-color="white"/>\n' +
' <mj-section padding="0px 0px" />\n' +
' <mj-text padding="0px 10px" />\n' +
' <mj-image padding="10px 10px" alt="" />\n' +
' <mj-mosaico-image padding="10px 10px" alt="" />\n' +
' <mj-mosaico-button align="left" background-color="#e85034" color="#fff" border-radius="24px" font-size="11px" />\n' +
' \n' +
' <mj-class name="header-section" margin-top="10px" background-color="#e85034" padding-top="5px" padding-bottom="5px" full-width="full-width" css-class="header"/>\n' +
'\n' +
' <mj-class name="banner-section" padding-top="10px" />\n' +
'\n' +
' <mj-class name="feature-section" padding-top="10px" padding-bottom="15px" css-class="feature" />\n' +
' \n' +
' <mj-class name="divider-section" background-color="#e85034" vertical-align="middle" full-width="full-width" padding-top="10px" padding-bottom="5px" css-class="divider"/>\n' +
'\n' +
' <mj-class name="article-section" padding-top="10px" padding-bottom="10px" css-class="article" />\n' +
'\n' +
' <mj-class name="links-section" padding-top="0px" padding-bottom="0px" background-color="#e85034" vertical-align="middle" full-width="full-width" />\n' +
' <mj-social font-size="12px" font-family="arial,helvetica neue,helvetica,sans-serif" icon-size="30px" mode="horizontal" />\n' +
' <mj-social-element text-padding="4px 15px 4px 0px" padding="8px" alt="" color="white"/>\n' +
' \n' +
' <mj-class name="footer-section" padding-top="30px"/>\n' +
' <mj-class name="footer-text" css-class="footer" />\n' +
' </mj-attributes>\n' +
' \n' +
' <mj-mosaico-property property-id="leftImage" label="Left Image" type="image" />\n' +
' <mj-mosaico-property property-id="middleImage" label="Middle Image" type="image" />\n' +
' <mj-mosaico-property property-id="rightImage" label="Right Image" type="image" />\n' +
' <mj-mosaico-property property-id="readMoreLink" label="Button" type="link" />\n' +
' \n' +
' <mj-style>\n' +
' p {\n' +
' font-family: Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif;\n' +
' font-size: 12px;\n' +
' color: #9da3a3;\n' +
' margin-top: 8px;\n' +
' }\n' +
' \n' +
' h2 {\n' +
' margin: 5px 0px 0px 0px;\n' +
' font-size: 15px;\n' +
' font-weight: normal;\n' +
' }\n' +
' \n' +
' .header a {\n' +
' text-decoration: none;\n' +
' color: white;\n' +
' }\n' +
' \n' +
' .feature p {\n' +
' text-align: center;\n' +
' }\n' +
' \n' +
' .feature h2 {\n' +
' color: #e85034;\n' +
' text-align: center;\n' +
' }\n' +
' \n' +
' .divider h2 {\n' +
' color: white;\n' +
' text-align: center;\n' +
' }\n' +
' \n' +
' .divider p {\n' +
' color: #f8d5d1;\n' +
' text-align: center;\n' +
' }\n' +
'\n' +
' .article h2 {\n' +
' font-weight: bold;\n' +
' margin-top: 10px;\n' +
' }\n' +
' \n' +
' .article p {\n' +
' color: #45474e;\n' +
' }\n' +
'\n' +
' .footer a {\n' +
' color: #3A3A3A;\n' +
' }\n' +
'\n' +
' .footer p {\n' +
' font-family: arial,helvetica neue,helvetica,sans-serif;\n' +
' font-size: 12px;\n' +
' text-align: center;\n' +
' }\n' +
' </mj-style>\n' +
' </mj-head>\n' +
' \n' +
' <mj-body>\n' +
' \n' +
' <mj-section mj-class="header-section">\n' +
' <mj-column>\n' +
' <mj-text align="left"><a href="https://lists.example.org/subscription/[LIST_ID]?locale=en-US">Subscribe</a></mj-text>\n' +
' </mj-column>\n' +
' <mj-column>\n' +
' <mj-text align="right"><a href="[LINK_BROWSER]">View&nbsp;in&nbsp;browser</a></mj-text>\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' \n' +
' <mj-mosaico-block block-id="banner" label="Banner">\n' +
' <mj-section mj-class="banner-section">\n' +
' <mj-column>\n' +
' <mj-mosaico-image property-id="image" placeholder-height="400" href="https://www.example.com/xxx" />\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' </mj-mosaico-block>\n' +
'\n' +
' <mj-mosaico-container>\n' +
' \n' +
' <mj-mosaico-block block-id="feature_section" label="Feature">\n' +
' <mj-section mj-class="feature-section">\n' +
' <mj-column>\n' +
' <mj-mosaico-image property-id="leftImage" placeholder-height="150" href-editable="true" />\n' +
' <mj-text>\n' +
' <h2 mj-mosaico-editable="leftTitleText">Lorem ipsum dolor</h2>\n' +
' <div mj-mosaico-editable="leftBodyText">\n' +
' <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eleifend sagittis nunc, et fermentum est ullamcorper dignissim.</p>\n' +
' </div>\n' +
' </mj-text>\n' +
' </mj-column>\n' +
' <mj-column>\n' +
' <mj-mosaico-image property-id="middleImage" placeholder-height="150" href-editable="true" />\n' +
' <mj-text>\n' +
' <h2 mj-mosaico-editable="middleTitleText">Lorem ipsum dolor</h2>\n' +
' <div mj-mosaico-editable="leftBodyText">\n' +
' <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eleifend sagittis nunc, et fermentum est ullamcorper dignissim.</p>\n' +
' </div>\n' +
' </mj-text>\n' +
' </mj-column>\n' +
' <mj-column>\n' +
' <mj-mosaico-image property-id="rightImage" placeholder-height="150" href-editable="true" />\n' +
' <mj-text>\n' +
' <h2 mj-mosaico-editable="rightTitleText">Lorem ipsum dolor</h2>\n' +
' <div mj-mosaico-editable="leftBodyText">\n' +
' <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eleifend sagittis nunc, et fermentum est ullamcorper dignissim.</p>\n' +
' </div>\n' +
' </mj-text>\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' </mj-mosaico-block>\n' +
'\n' +
' <mj-mosaico-block block-id="divider_section" label="Divider">\n' +
' <mj-section mj-class="divider-section">\n' +
' <mj-column>\n' +
' <mj-text>\n' +
' <h2 mj-mosaico-editable="titleText">Lorem ipsum dolor</h2>\n' +
' </mj-text>\n' +
' <mj-divider border-color="white" border-width="1px" padding-bottom="10px" padding-top="15px"></mj-divider>\n' +
' <mj-text>\n' +
' <div mj-mosaico-editable="bodyText">\n' +
' <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>\n' +
' </div>\n' +
' </mj-text>\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' </mj-mosaico-block> \n' +
' \n' +
' <mj-mosaico-block block-id="article_section" label="Article">\n' +
' <mj-section mj-class="article-section">\n' +
' <mj-column>\n' +
' <mj-mosaico-image property-id="image" placeholder-height="280" href-editable="true" />\n' +
' </mj-column>\n' +
' <mj-column>\n' +
' <mj-text>\n' +
' <h2 mj-mosaico-editable="titleText">Lorem ipsum dolor</h2>\n' +
' <div mj-mosaico-editable="bodyText">\n' +
' <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>\n' +
' </div>\n' +
' </mj-text>\n' +
' <mj-mosaico-button property-id="readMoreLink" font-family="Ubuntu, Helvetica, Arial, sans-serif, Helvetica, Arial, sans-serif" padding-left="10px" padding-bottom="15px" padding-top="15px">Read more ...</mj-mosaico-button>\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' </mj-mosaico-block> \n' +
' \n' +
' </mj-mosaico-container> \n' +
'\n' +
' <mj-section mj-class="links-section">\n' +
' <mj-column>\n' +
' <mj-social border-radius="5px">\n' +
' <mj-social-element name="facebook" href="[LINK_BROWSER]">Share on Facebook</mj-social-element>\n' +
' <mj-social-element name="twitter" href="[LINK_BROWSER]">Tweet</mj-social-element>\n' +
' </mj-social> \n' +
' </mj-column>\n' +
' </mj-section>\n' +
' \n' +
' <mj-section mj-class="footer-section">\n' +
' <mj-column>\n' +
' <mj-text mj-class="footer-text">\n' +
' <p>This email was sent to <a href="mailto:[EMAIL]">[EMAIL]</a><p>\n' +
' <p> &nbsp; <a href="[LINK_UNSUBSCRIBE]">Unsubscribe&nbsp;from&nbsp;this&nbsp;list</a> &nbsp; &nbsp; <a href="[LINK_PREFERENCES]">Update&nbsp;subscription&nbsp;preferences</a> &nbsp; </p>\n' +
' <p>Your address XXXXXX</p>\n' +
' </mj-text>\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' \n' +
' </mj-body>\n' +
'</mjml>';
function getVersafix() {
return versafix;
}
function getMJMLSample() {
return mjmlSample;
}
module.exports = {
getVersafix
getVersafix,
getMJMLSample
};