Added ability to make a conditional block in MJML Mosaico.

Mosaico switched from master to v0.17.5
Added workaround for Chrome - after save, images in Mosaico disappear
This commit is contained in:
Tomas Bures 2019-05-19 01:42:26 +02:00
parent a527b80291
commit 2e9d44c705
12 changed files with 1483 additions and 1304 deletions

View file

@ -57,7 +57,7 @@ class MosaicoSandbox extends Component {
...
</div>
*/
let html = this.viewModel.exportHTML();
let html = this.viewModel.export();
html = juice(html);
return {
@ -86,6 +86,18 @@ class MosaicoSandbox extends Component {
viewModel.originalExportHTML = viewModel.exportHTML;
viewModel.exportHTML = () => {
let html = viewModel.originalExportHTML();
// Chrome workaround begin -----------------------------------------------------------------------------------
// Chrome v. 74 (and likely other versions too) has problem with how KO sets data during export.
// As the result, the images that have been in the template from previous editing (i.e. before page refresh)
// get lost. The code below refreshes the KO binding, thus effectively reloading the images.
const isChrome = !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime);
if (isChrome) {
ko.cleanNode(document.body);
ko.applyBindings(viewModel, document.body);
}
// Chrome workaround end -------------------------------------------------------------------------------------
for (const portRender of window.mosaicoHTMLPostRenderers) {
html = postRender(html);
}

View file

@ -199,8 +199,14 @@ export default class CUD extends Component {
{isEdit && typeKey && this.templateTypes[typeKey].getForm(this)}
<ButtonRow>
<Button type="submit" className="btn-primary" icon="check" label={t('save')}/>
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(true)}/>
{isEdit ?
<>
<Button type="submit" className="btn-primary" icon="check" label={t('save')}/>
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndLeave')} onClickAsync={async () => await this.submitHandler(true)}/>
</>
:
<Button type="submit" className="btn-primary" icon="check" label={t('saveAndEditContent')}/>
}
{canDelete && <LinkButton className="btn-danger" icon="trash-alt" label={t('delete')} to={`/templates/mosaico/${this.props.entity.id}/delete`}/>}
{isEdit && typeKey && this.templateTypes[typeKey].getButtons(this)}
</ButtonRow>

View file

@ -26,7 +26,7 @@ function getParent() {
}
function handleMosaicoEditable(block, src) {
function handleMosaicoAttributes(block, src) {
let newSrc = src;
let offset = 0;
@ -43,13 +43,15 @@ function handleMosaicoEditable(block, src) {
let newFragment = tagStr;
for (const attrMatch of attrsMatches) {
if (attrMatch[1] === 'mj-mosaico-editable') {
const propertyId = attrMatch[2];
block.addMosaicoProperty(propertyId);
newFragment += ` data-ko-editable="${propertyId}"`;
} else if (attrMatch[1] === 'mj-mosaico-display') {
const propertyId = attrMatch[2];
block.addMosaicoProperty(propertyId);
newFragment += ` data-ko-display="${propertyId}"`;
} else {
newFragment += ` ${attrMatch[0]}`;
}
@ -75,35 +77,39 @@ function handleMosaicoEditable(block, src) {
class MjMosaicoProperty extends HeadComponent {
static endingTag = true
static endingTag = true;
static allowedAttributes = {
'property-id': 'string',
label: 'string',
type: 'enum(image,link)'
}
type: 'enum(image,link,visible)'
};
handler() {
const { add } = this.context;
let properties = null;
let extend = null;
const type = this.getAttribute('type');
if (type === 'image') {
properties = 'src url alt';
} else if (type === 'link') {
properties = 'text url';
} else if (type === 'visible') {
extend = 'visible';
}
const propertiesStr = properties ? ` properties: ${properties}` : '';
const extendStr = extend ? ` extend: ${extend}` : '';
add('style', ` @supports -ko-blockdefs { ${this.getAttribute('property-id')} { label: ${this.getAttribute('label')};${propertiesStr} } }`);
add('style', ` @supports -ko-blockdefs { ${this.getAttribute('property-id')} { label: ${this.getAttribute('label')};${propertiesStr}${extendStr} } }`);
}
}
class MjMosaicoContainer extends BodyComponent {
static endingTag = false
static endingTag = false;
render() {
return `
@ -114,6 +120,90 @@ class MjMosaicoContainer extends BodyComponent {
}
}
class MjMosaicoConditionalDisplay extends BodyComponent {
constructor(initialDatas = {}) {
super(initialDatas);
const propertyId = this.getAttribute('property-id');
this.propertyId = propertyId || `display_${getId()}`;
const parentBlock = getParent();
if (parentBlock) {
parentBlock.addMosaicoProperty(this.propertyId);
}
}
componentHeadStyle = breakpoint => {
const label = this.getAttribute('label');
if (label) {
return `
@supports -ko-blockdefs {
${this.propertyId} { label: ${label}; extend: visible }
}
`;
} else {
return '';
}
};
static endingTag = false;
static allowedAttributes = {
'property-id': 'string',
'label': 'string'
};
render() {
const { children } = this.props;
return `
<div
${this.htmlAttributes({
"data-ko-display": this.propertyId,
class: this.getAttribute('css-class'),
'data-ko-block': this.blockId
})}
>
<table
${this.htmlAttributes({
border: '0',
cellpadding: '0',
cellspacing: '0',
role: 'presentation',
style: 'table',
width: '100%',
})}
>
${this.renderChildren(children, {
renderer: component => component.constructor.isRawElement() ? component.render() : `
<tr>
<td
${component.htmlAttributes({
align: component.getAttribute('align'),
'vertical-align': component.getAttribute('vertical-align'),
class: component.getAttribute('css-class'),
style: {
background: component.getAttribute('container-background-color'),
'font-size': '0px',
padding: component.getAttribute('padding'),
'padding-top': component.getAttribute('padding-top'),
'padding-right': component.getAttribute('padding-right'),
'padding-bottom': component.getAttribute('padding-bottom'),
'padding-left': component.getAttribute('padding-left'),
'word-break': 'break-word',
},
})}
>
${component.render()}
</td>
</tr>
`
})}
</table>
</div>
`;
}
}
class MjMosaicoBlock extends BodyComponent {
constructor(initialDatas = {}) {
@ -132,14 +222,14 @@ class MjMosaicoBlock extends BodyComponent {
${this.blockId} { label: ${this.getAttribute('label')}${propertiesOut} }
}
`;
}
};
static endingTag = false
static endingTag = false;
static allowedAttributes = {
'block-id': 'string',
'label': 'string'
}
};
addMosaicoProperty(property) {
this.mosaicoProperties.push(property);
@ -159,7 +249,7 @@ class MjMosaicoBlock extends BodyComponent {
`;
popParent();
return handleMosaicoEditable(this, result);
return handleMosaicoAttributes(this, result);
}
}
@ -181,14 +271,14 @@ class MjMosaicoInnerBlock extends BodyComponent {
${this.blockId} { label: ${this.getAttribute('label')}${propertiesOut} }
}
`;
}
};
static endingTag = false
static endingTag = false;
static allowedAttributes = {
'block-id': 'string',
'label': 'string'
}
};
addMosaicoProperty(property) {
this.mosaicoProperties.push(property);
@ -238,14 +328,14 @@ class MjMosaicoInnerBlock extends BodyComponent {
${component.render()}
</td>
</tr>
`,
`
})}
</table>
</div>
`;
popParent();
return handleMosaicoEditable(this, result);
return handleMosaicoAttributes(this, result);
}
}
@ -264,7 +354,7 @@ class MjMosaicoImage extends BodyComponent {
}
}
static tagOmission = true
static tagOmission = true;
static allowedAttributes = {
'property-id': 'string',
@ -292,7 +382,7 @@ class MjMosaicoImage extends BodyComponent {
target: 'string',
width: 'unit(px)',
height: 'unit(px)',
}
};
static defaultAttributes = {
align: 'center',
@ -302,7 +392,7 @@ class MjMosaicoImage extends BodyComponent {
target: '_blank',
'placeholder-height': '400',
'href-editable': false
}
};
getStyles() {
const width = this.getContentWidth();
@ -453,7 +543,7 @@ class MjMosaicoButton extends BodyComponent {
}
}
static endingTag = true
static endingTag = true;
static allowedAttributes = {
'property-id': 'string',
@ -487,7 +577,7 @@ class MjMosaicoButton extends BodyComponent {
'vertical-align': 'enum(top,bottom,middle)',
'text-align': 'enum(left,right,center)',
width: 'unit(px,%)',
}
};
static defaultAttributes = {
align: 'center',
@ -505,7 +595,7 @@ class MjMosaicoButton extends BodyComponent {
'text-decoration': 'none',
'text-transform': 'none',
'vertical-align': 'middle',
}
};
getStyles() {
return {
@ -591,6 +681,7 @@ class MjMosaicoButton extends BodyComponent {
const mjmlInstance = new MJML();
mjmlInstance.registerComponent(MjMosaicoContainer);
mjmlInstance.registerComponent(MjMosaicoConditionalDisplay);
mjmlInstance.registerComponent(MjMosaicoBlock);
mjmlInstance.registerComponent(MjMosaicoInnerBlock);
mjmlInstance.registerComponent(MjMosaicoImage);
@ -601,8 +692,13 @@ 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-mosaico-button'],
'mj-column': ['mj-mosaico-container', 'mj-mosaico-inner-block', 'mj-mosaico-image', 'mj-mosaico-button', 'mj-mosaico-conditional-display'],
'mj-mosaico-block': ['mj-section', 'mj-column'],
'mj-mosaico-conditional-display': [
'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-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',
@ -614,6 +710,8 @@ mjmlInstance.registerDependencies({
mjmlInstance.addToHeader(`
<style type="text/css">
@supports -ko-blockdefs {
visible { label: Visible?; widget: boolean }
color { label: Color; widget: color }
text { label: Paragraph; widget: text }
image { label: Image; properties: src url alt }
link { label: Link; properties: text url }