enrollment-app/web/email-editor.html
2026-07-02 15:39:19 +05:30

257 lines
7.3 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Email Editor</title>
<link rel="stylesheet" href="https://unpkg.com/grapesjs/dist/css/grapes.min.css" />
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background: #f5f5f5;
font-family: Arial, Helvetica, sans-serif;
}
#gjs { height: 100%; border: none; }
.gjs-one-bg { background-color: #f8f9fa; }
.gjs-two-color { color: #333; }
.gjs-three-bg { background-color: #009195; color: #fff; }
.gjs-four-color,
.gjs-four-color-h:hover { color: #009195; }
.gjs-pn-btn.gjs-pn-active,
.gjs-pn-btn:hover { color: #009195; }
.gjs-block { min-height: 54px; }
</style>
</head>
<body>
<div id="gjs"></div>
<script src="https://unpkg.com/grapesjs"></script>
<script src="https://unpkg.com/grapesjs-preset-newsletter"></script>
<script>
let editor = null;
let pendingHtml = null;
let changeTimer = null;
let lastExportedHtml = '';
let destroyed = false;
function postToParent(payload) {
if (destroyed) return;
if (window.parent && window.parent !== window) {
window.parent.postMessage(JSON.stringify(payload), '*');
}
}
function extractBodyContent(html) {
if (!html) return '';
const match = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
return match ? match[1].trim() : html;
}
function wrapAsEmailDocument(innerHtml) {
return '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n</head>\n<body style="margin:0;padding:0;">' + innerHtml + '\n</body>\n</html>';
}
function exportFullHtml() {
if (!editor) return '';
try {
const inlined = editor.runCommand('gjs-get-inlined-html');
return wrapAsEmailDocument(inlined || '');
} catch (e) {
const html = editor.getHtml() || '';
const css = editor.getCss() || '';
return wrapAsEmailDocument('<style>' + css + '</style>' + html);
}
}
function scheduleChange() {
if (destroyed) return;
clearTimeout(changeTimer);
changeTimer = setTimeout(function () {
if (destroyed || !editor) return;
const html = exportFullHtml();
if (!html || html === lastExportedHtml) return;
lastExportedHtml = html;
postToParent({ source: 'nhance-email-editor', action: 'change', html: html });
}, 800);
}
function loadHtml(fullHtml) {
if (destroyed || !editor) {
pendingHtml = fullHtml;
return;
}
const content = extractBodyContent(fullHtml);
editor.setComponents(content);
makePlaceholderTokensEditable();
editor.refresh();
lastExportedHtml = exportFullHtml();
}
function makePlaceholderTokensEditable() {
if (destroyed || !editor) return;
const wrapper = editor.getWrapper();
if (!wrapper) return;
const visit = function (cmp) {
if (!cmp || typeof cmp.get !== 'function') return;
const content = (cmp.get('content') || '').toString();
if (/\{\{[^{}]+\}\}/.test(content)) {
cmp.set('editable', true);
cmp.set('selectable', true);
cmp.set('hoverable', true);
cmp.set('copyable', true);
}
const children = cmp.components && cmp.components();
if (children && typeof children.each === 'function') {
children.each(function (child) {
visit(child);
});
}
};
visit(wrapper);
// Ensure rendered canvas nodes containing placeholders are editable too.
// Some newsletter/table components keep internal text nodes read-only unless
// contenteditable is explicitly set on the live DOM element.
const doc = editor.Canvas && editor.Canvas.getDocument
? editor.Canvas.getDocument()
: null;
if (!doc) return;
const nodes = doc.querySelectorAll('*');
nodes.forEach(function (el) {
if (!el || !el.innerHTML) return;
if (/\{\{[^{}]+\}\}/.test(el.innerHTML)) {
el.setAttribute('contenteditable', 'true');
}
});
}
function insertToken(token) {
if (destroyed || !editor) return;
const selected = editor.getSelected();
const cmp = selected && selected.get('type') !== 'wrapper'
? selected
: editor.getWrapper().components().at(0);
if (cmp && cmp.append) {
cmp.append({
type: 'text',
content: token,
editable: true,
selectable: true,
hoverable: true,
copyable: true,
});
} else {
editor.addComponents({
type: 'text',
content: token,
editable: true,
selectable: true,
hoverable: true,
copyable: true,
});
}
makePlaceholderTokensEditable();
scheduleChange();
}
function destroyEditor() {
destroyed = true;
clearTimeout(changeTimer);
changeTimer = null;
try {
if (editor) editor.destroy();
} catch (e) {}
editor = null;
}
function initEditor() {
try {
editor = grapesjs.init({
container: '#gjs',
height: '100%',
width: 'auto',
fromElement: false,
storageManager: false,
noticeOnUnload: false,
canvas: {
styles: ['https://fonts.googleapis.com/css?family=Roboto:400,500,700'],
},
deviceManager: {
devices: [
{ name: 'Desktop', width: '' },
{ name: 'Mobile', width: '320px', widthMedia: '480px' },
],
},
plugins: ['grapesjs-preset-newsletter'],
pluginsOpts: {
'grapesjs-preset-newsletter': {
modalLabelImport: 'Paste your HTML',
modalLabelExport: 'Copy HTML',
codeViewerTheme: 'material',
},
},
});
editor.on('update', scheduleChange);
editor.on('component:selected', function () {
makePlaceholderTokensEditable();
});
if (pendingHtml) {
loadHtml(pendingHtml);
pendingHtml = null;
}
postToParent({ source: 'nhance-email-editor', action: 'ready' });
} catch (e) {
postToParent({ source: 'nhance-email-editor', action: 'error', message: String(e) });
}
}
window.addEventListener('message', function (event) {
let data = event.data || {};
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (_) {
return;
}
}
if (data.source !== 'nhance-email-editor-host') return;
switch (data.action) {
case 'load':
loadHtml(data.html || '');
break;
case 'export':
postToParent({
source: 'nhance-email-editor',
action: 'html',
requestId: data.requestId,
html: exportFullHtml(),
});
break;
case 'insertToken':
insertToken(data.token || '');
break;
case 'destroy':
destroyEditor();
break;
default:
break;
}
});
initEditor();
</script>
</body>
</html>