diff --git a/internal/web/handlers/templates.go b/internal/web/handlers/templates.go index 802ae45..c22d830 100644 --- a/internal/web/handlers/templates.go +++ b/internal/web/handlers/templates.go @@ -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", @@ -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 + }) +} + // TemplateExportData represents template data for export/import type TemplateExportData struct { Name string `json:"name"` diff --git a/internal/web/static/js/builder.js b/internal/web/static/js/builder.js index a8bc06d..69b3465 100644 --- a/internal/web/static/js/builder.js +++ b/internal/web/static/js/builder.js @@ -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 '' + varName + ''; }); } @@ -407,6 +412,7 @@ function previewHTML(html) { html = applyTestValues(html); html = replaceImageVars(html); + html = replaceHrefVars(html); html = replaceTextVars(html); return html; } @@ -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; + } + 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); }); } @@ -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(); @@ -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, diff --git a/internal/web/views/block_form.html b/internal/web/views/block_form.html index 98f6f9f..75a4d6e 100644 --- a/internal/web/views/block_form.html +++ b/internal/web/views/block_form.html @@ -321,6 +321,7 @@