Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/web/handlers/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,8 @@ func (h *Handlers) TemplatePreview(w http.ResponseWriter, r *http.Request) {
}
}

html = stripDarkModeCSS(html)

data := map[string]any{
"Title": "Preview: " + t.Name,
"Active": "templates",
Expand All @@ -483,6 +485,17 @@ func (h *Handlers) TemplatePreview(w http.ResponseWriter, r *http.Request) {
h.render(w, "template_preview", data)
}

var darkModeBlockRe = regexp.MustCompile(`(?s)@media\s*\(prefers-color-scheme:\s*dark\)\s*\{.*?\}\s*\}`)

func stripDarkModeCSS(html string) string {
return darkModeBlockRe.ReplaceAllStringFunc(html, func(m string) string {
if strings.Contains(m, ".force-page-bg") {
return ""
}
return m
})
}
Comment on lines +488 to +497

// TemplateExportData represents template data for export/import
type TemplateExportData struct {
Name string `json:"name"`
Expand Down
121 changes: 99 additions & 22 deletions internal/web/static/js/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,13 @@
});
}

function replaceHrefVars(html) {
return html.replace(/href\s*=\s*(['"])\s*\{\{\s*\.\w+(?:\.\w+)*\s*\}\}\s*\1/gi, 'href=$1#$1');
}

function replaceTextVars(html) {
return html.replace(/\{\{\.(\w+)\}\}/g, function(match, varName) {
return html.replace(/<[^>]*>|\{\{\.(\w+)\}\}/g, function(match, varName) {
if (match[0] === '<') return match; // a tag — leave untouched
return '<span data-placeholder="' + varName + '" style="background:#E4E4E4;color:#959595;padding:1px 4px;border-radius:3px;font-size:12px;">' + varName + '</span>';
});
}
Expand Down Expand Up @@ -407,6 +412,7 @@
function previewHTML(html) {
html = applyTestValues(html);
html = replaceImageVars(html);
html = replaceHrefVars(html);
html = replaceTextVars(html);
return html;
}
Expand All @@ -432,39 +438,82 @@
}, true);
}

function bindEditable(el, original, item, previewEl, srcIndex) {
el.setAttribute('contenteditable', 'true');
el.setAttribute('spellcheck', 'false');
el.dataset.originalText = original;
if (srcIndex !== undefined && srcIndex >= 0) el.dataset.sourceIndex = String(srcIndex);
el.addEventListener('mousedown', function(e) { e.stopPropagation(); });
el.addEventListener('focus', function() {
var blockEl = el.closest('.builder-block');
if (blockEl) blockEl.draggable = false;
});
el.addEventListener('input', function() { scheduleInlineSave(item, previewEl); });
el.addEventListener('blur', function() {
var blockEl = el.closest('.builder-block');
if (blockEl) blockEl.draggable = true;
scheduleInlineSave(item, previewEl, true);
});
el.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); el.blur(); }
});
}

function enableInlineEdit(previewEl, item) {
var cursor = 0;
var locate = function(text) {
var at = item.html.indexOf(text, cursor);
if (at === -1) return -1;
cursor = at + text.length;
return at;
};
previewEl.querySelectorAll('td, th, span, p, h1, h2, h3, h4, h5, h6, li, a, b, strong, i, em').forEach(function(node) {
if (node.dataset && node.dataset.placeholder) return;
if (node.dataset && node.dataset.inlineText) return;
if (node.dataset && node.dataset.inlineDone) return;
var mixed = false;
var blocksParent = false;
for (var c = 0; c < node.children.length; c++) {
var ch = node.children[c];
if (ch.tagName === 'BR') continue;
if (ch.tagName === 'IMG' || ch.tagName === 'A') { mixed = true; continue; }
if (ch.dataset && ch.dataset.placeholder) continue;
blocksParent = true;
break;
}
if (blocksParent) return;

if (mixed) {
Array.prototype.slice.call(node.childNodes).forEach(function(ch) {
if (ch.nodeType === 3) {
var raw = ch.nodeValue;
var trimmed = raw && raw.trim();
if (!trimmed) return;
var at = locate(trimmed);
if (at === -1) return;
var span = document.createElement('span');
span.dataset.inlineText = '1';
span.textContent = raw;
ch.parentNode.replaceChild(span, ch);
bindEditable(span, trimmed, item, previewEl, at);
} else if (ch.nodeType === 1 && ch.tagName === 'A') {
var aText = canonicalText(ch).trim();
if (!aText) return;
var aAt = locate(aText);
if (aAt === -1) return;
bindEditable(ch, aText, item, previewEl, aAt);
ch.dataset.inlineDone = '1';
}
});
return;
Comment thread
foxzi marked this conversation as resolved.
}

var canonical = canonicalText(node);
var trimmed = canonical.trim();
if (!trimmed) return;
if (item.html.indexOf(trimmed) === -1) return;
node.setAttribute('contenteditable', 'true');
node.setAttribute('spellcheck', 'false');
node.dataset.originalText = trimmed;
node.addEventListener('mousedown', function(e) { e.stopPropagation(); });
node.addEventListener('focus', function() {
var blockEl = node.closest('.builder-block');
if (blockEl) blockEl.draggable = false;
});
node.addEventListener('input', function() { scheduleInlineSave(item, previewEl); });
node.addEventListener('blur', function() {
var blockEl = node.closest('.builder-block');
if (blockEl) blockEl.draggable = true;
scheduleInlineSave(item, previewEl, true);
});
node.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); node.blur(); }
});
var at = locate(trimmed);
if (at === -1) return;
bindEditable(node, trimmed, item, previewEl, at);
});
}

Expand All @@ -479,6 +528,8 @@
var newSourceHTML = item.html;
var changes = 0;
var diagnostics = [];

var edits = [];
previewEl.querySelectorAll('[contenteditable="true"]').forEach(function(node) {
var original = node.dataset.originalText;
var current = canonicalText(node).trim();
Expand All @@ -489,15 +540,41 @@
diagnostics.push('placeholder count mismatch in ' + JSON.stringify(original.slice(0, 30)));
return;
}
var idx = newSourceHTML.indexOf(original);
var idx = -1;
if (node.dataset.sourceIndex !== undefined) {
var si = parseInt(node.dataset.sourceIndex, 10);
if (si >= 0 && item.html.substr(si, original.length) === original) idx = si;
}
if (idx === -1) idx = item.html.indexOf(original);
if (idx === -1) {
diagnostics.push('original text not found in source: ' + JSON.stringify(original.slice(0, 30)));
return;
}
newSourceHTML = newSourceHTML.slice(0, idx) + current + newSourceHTML.slice(idx + original.length);
node.dataset.originalText = current;
edits.push({ idx: idx, original: original, current: current, node: node });
});

edits.sort(function(a, b) { return a.idx - b.idx; });
var offset = 0;
edits.forEach(function(e) {
var pos = e.idx + offset;
newSourceHTML = newSourceHTML.slice(0, pos) + e.current + newSourceHTML.slice(pos + e.original.length);
offset += e.current.length - e.original.length;
e.node.dataset.originalText = e.current;
changes++;
});

if (changes > 0) {
var recur = 0;
previewEl.querySelectorAll('[contenteditable="true"]').forEach(function(node) {
if (node.dataset.sourceIndex === undefined) return;
var t = node.dataset.originalText;
if (t === undefined) return;
var at = newSourceHTML.indexOf(t, recur);
if (at === -1) return;
node.dataset.sourceIndex = String(at);
recur = at + t.length;
});
}
console.log('[inline-edit] save called', {
blockId: item.blockId,
sourceLen: item.html.length,
Expand Down
1 change: 1 addition & 0 deletions internal/web/views/block_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ <h3>Template Variables</h3>
if (!r.ok) return r.text().then(function(t) { throw new Error(t); });
return r.text();
}).then(function(html) {
html = html.replace(/@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[\s\S]*?\}\s*\}/g, function(m){ return m.indexOf('.force-page-bg') !== -1 ? '' : m; });
frame.srcdoc = html;
status.textContent = 'OK — block rendered with the JSON above.';
status.style.color = 'var(--text-muted)';
Expand Down
1 change: 1 addition & 0 deletions internal/web/views/block_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ <h2 class="card-title">Appearance</h2>
if (!r.ok) return r.text().then(function(t) { throw new Error(t); });
return r.text();
}).then(function(html) {
html = html.replace(/@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[\s\S]*?\}\s*\}/g, function(m){ return m.indexOf('.force-page-bg') !== -1 ? '' : m; });
frame.srcdoc = html;
status.textContent = 'OK';
status.style.color = 'var(--text-muted)';
Expand Down
4 changes: 1 addition & 3 deletions internal/web/views/template_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,6 @@ <h2>Deployment Status</h2>
</div>
</div>

sample payload so Subject macros, range and if blocks expand the
same way they will when the API serves a real recipient. The width
buttons resize the surrounding shell so the wrapper @media rules
<div class="card">
<div class="card-header">
<h2>Preview</h2>
Expand Down Expand Up @@ -229,6 +226,7 @@ <h2>Actions</h2>
if (!r.ok) return r.text().then(function(t) { throw new Error(t); });
return r.text();
}).then(function(rendered) {
rendered = rendered.replace(/@media\s*\(prefers-color-scheme:\s*dark\)\s*\{[\s\S]*?\}\s*\}/g, function(m){ return m.indexOf('.force-page-bg') !== -1 ? '' : m; });
frame.srcdoc = rendered;
if (status) { status.textContent = 'OK'; status.style.color = 'var(--text-muted)'; }
}).catch(function(err) {
Expand Down
Loading