mailtrain/client/src/lib/sandboxed-codeeditor-root.js
2018-12-15 20:09:07 +01:00

228 lines
6.9 KiB
JavaScript

'use strict';
import './public-path';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {I18nextProvider} from 'react-i18next';
import i18n, {withTranslation} from './i18n';
import {
parentRPC,
UntrustedContentRoot
} from './untrusted';
import PropTypes
from "prop-types";
import styles
from "./sandboxed-codeeditor.scss";
import {
getPublicUrl,
getSandboxUrl,
getTrustedUrl
} from "./urls";
import {
base,
unbase
} from "../../../shared/templates";
import ACEEditorRaw
from 'react-ace';
import 'brace/theme/github';
import 'brace/ext/searchbox';
import 'brace/mode/html';
import {CodeEditorSourceType} from "./sandboxed-codeeditor-shared";
import mjml2html
from "mjml4-in-browser";
import juice
from "juice";
const refreshTimeout = 1000;
@withTranslation()
class CodeEditorSandbox extends Component {
constructor(props) {
super(props);
let defaultSource;
if (props.sourceType === CodeEditorSourceType.MJML) {
defaultSource =
'<mjml>\n' +
' <mj-body>\n' +
' <mj-section>\n' +
' <mj-column>\n' +
' <!-- First column content -->\n' +
' </mj-column>\n' +
' <mj-column>\n' +
' <!-- Second column content -->\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' </mj-body>\n' +
'</mjml>';
} else if (props.sourceType === CodeEditorSourceType.HTML) {
defaultSource =
'<!DOCTYPE html>\n' +
'<html>\n' +
'<head>\n' +
' <meta charset="UTF-8">\n' +
' <title>Title of the document</title>\n' +
'</head>\n' +
'<body>\n' +
' Content of the document......\n' +
'</body>\n' +
'</html>';
}
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
const source = this.props.initialSource ? base(this.props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource;
this.state = {
source,
preview: props.initialPreview,
wrapEnabled: props.initialWrap
};
this.state.previewContents = this.getHtml();
this.onCodeChangedHandler = ::this.onCodeChanged;
this.refreshHandler = ::this.refresh;
this.refreshTimeoutId = null;
this.onMessageFromPreviewHandler = ::this.onMessageFromPreview;
this.previewScroll = {x: 0, y: 0};
}
static propTypes = {
entityTypeId: PropTypes.string,
entityId: PropTypes.number,
initialSource: PropTypes.string,
sourceType: PropTypes.string,
initialPreview: PropTypes.bool,
initialWrap: PropTypes.bool
}
async exportState(method, params) {
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
return {
html: unbase(this.getHtml(), trustedUrlBase, sandboxUrlBase, publicUrlBase, true),
source: unbase(this.state.source, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
};
}
async setPreview(method, preview) {
this.setState({
preview
});
}
async setWrap(method, wrap) {
this.setState({
wrapEnabled: wrap
});
}
componentDidMount() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
parentRPC.setMethodHandler('setPreview', ::this.setPreview);
parentRPC.setMethodHandler('setWrap', ::this.setWrap);
window.addEventListener('message', this.onMessageFromPreviewHandler, false);
}
componentWillUnmount() {
clearTimeout(this.refreshTimeoutId);
}
getHtml() {
let previewContents;
if (this.props.sourceType === CodeEditorSourceType.MJML) {
const res = mjml2html(this.state.source);
previewContents = res.html;
} else if (this.props.sourceType === CodeEditorSourceType.HTML) {
previewContents = juice(this.state.source);
}
return previewContents;
}
onCodeChanged(data) {
this.setState({
source: data
});
if (!this.refreshTimeoutId) {
this.refreshTimeoutId = setTimeout(() => this.refresh(), refreshTimeout);
}
}
onMessageFromPreview(evt) {
if (evt.data.type === 'scroll') {
this.previewScroll = evt.data.data;
}
}
refresh() {
this.refreshTimeoutId = null;
this.setState({
previewContents: this.getHtml()
});
}
render() {
const previewScript =
'(function() {\n' +
' function reportScroll() { window.parent.postMessage({type: \'scroll\', data: {x: window.scrollX, y: window.scrollY}}, \'*\'); }\n' +
' reportScroll();\n' +
' window.addEventListener(\'scroll\', reportScroll);\n' +
' window.addEventListener(\'load\', function(evt) { window.scrollTo(' + this.previewScroll.x + ',' + this.previewScroll.y +'); });\n' +
'})();\n';
const previewContents = this.state.previewContents.replace(/<\s*head\s*>/i, `<head><script>${previewScript}</script>`);
return (
<div className={styles.sandbox}>
<div className={this.state.preview ? styles.aceEditorWithPreview : styles.aceEditorWithoutPreview}>
<ACEEditorRaw
mode="html"
theme="github"
width="100%"
height="100%"
onChange={this.onCodeChangedHandler}
fontSize={12}
showPrintMargin={false}
value={this.state.source}
tabSize={2}
wrapEnabled={this.state.wrapEnabled}
setOptions={{useWorker: false}} // This disables syntax check because it does not always work well (e.g. in case of JS code in report templates)
/>
</div>
{
this.state.preview &&
<div className={styles.preview}>
<iframe ref={node => this.previewNode = node} src={"data:text/html;charset=utf-8," + encodeURIComponent(previewContents)}></iframe>
</div>
}
</div>
);
}
}
export default function() {
parentRPC.init();
ReactDOM.render(
<I18nextProvider i18n={ i18n }>
<UntrustedContentRoot render={props => <CodeEditorSandbox {...props} />} />
</I18nextProvider>,
document.getElementById('root')
);
};