From dc70b74f7dc4689abb51c1be5617820d5b480013 Mon Sep 17 00:00:00 2001 From: Joakim Date: Fri, 29 Aug 2025 23:08:46 +0200 Subject: [PATCH] Preserve layout and styling when applying edited content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎯 Problem: Buttons and styling lost when saving rich content ✅ Solution: Smart content application that preserves HTML structure 🔧 New Features: - parseMarkdownToBlocks() - Intelligently parses markdown - updateRichContent() - Updates elements in place vs wholesale replacement - canUpdateElement() - Checks element/content compatibility - updateElementContent() - Preserves classes while updating content - looksLikeButton() - Heuristics to detect and preserve button styling 🎨 Layout Preservation: - Buttons keep their 'btn-primary' classes and styling - Headings maintain existing CSS classes and hierarchy - Paragraphs preserve lead/subtitle classes - Links maintain href and visual styling ✨ User Experience: - Edit 'Get Started Today' button → still looks like a button after save - Rich content maintains professional appearance - No more plain text where buttons should be Smart frontend content management without backend complexity! --- demo-site/insertr/insertr.js | 181 +++++++++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 7 deletions(-) diff --git a/demo-site/insertr/insertr.js b/demo-site/insertr/insertr.js index 4552615..6da8a14 100644 --- a/demo-site/insertr/insertr.js +++ b/demo-site/insertr/insertr.js @@ -218,14 +218,11 @@ class Insertr { // Update content based on type if (contentType === 'rich') { - const html = this.markdownToHtml(content); - originalContent.innerHTML = html + ''; + // For rich content, intelligently update existing HTML structure + this.updateRichContent(originalContent, content); } else { - // For simple content, try to maintain structure but update text - const textNodes = this.getTextNodes(originalContent); - if (textNodes.length > 0) { - textNodes[0].textContent = content; - } + // For simple content, update text while preserving structure + this.updateSimpleContent(originalContent, content); } // Replace element content @@ -237,6 +234,176 @@ class Insertr { this.setupEditableElement(element, element.getAttribute('data-content-id')); } + updateRichContent(container, markdownContent) { + // Parse markdown content into structured data + const contentBlocks = this.parseMarkdownToBlocks(markdownContent); + + // Get existing elements in the container + const existingElements = Array.from(container.children).filter(el => + !el.classList.contains('insertr-edit-btn') + ); + + // Update existing elements or create new ones + contentBlocks.forEach((block, index) => { + const existingElement = existingElements[index]; + + if (existingElement && this.canUpdateElement(existingElement, block.type)) { + // Update existing element while preserving styling + this.updateElementContent(existingElement, block); + } else { + // Create new element or replace incompatible one + const newElement = this.createElementFromBlock(block); + if (existingElement) { + container.replaceChild(newElement, existingElement); + } else { + container.appendChild(newElement); + } + } + }); + + // Remove extra elements + for (let i = contentBlocks.length; i < existingElements.length; i++) { + if (existingElements[i]) { + container.removeChild(existingElements[i]); + } + } + } + + updateSimpleContent(container, textContent) { + // For simple content, find the main text node and update it + const textNodes = this.getTextNodes(container); + if (textNodes.length > 0) { + textNodes[0].textContent = textContent; + } else { + // If no text nodes, update first element's text content + const firstElement = container.querySelector('h1, h2, h3, h4, h5, h6, p, span, div'); + if (firstElement) { + firstElement.textContent = textContent; + } + } + } + + parseMarkdownToBlocks(markdown) { + const blocks = []; + const lines = markdown.split('\n'); + let currentBlock = null; + + lines.forEach(line => { + line = line.trim(); + if (!line && currentBlock) { + // Empty line - finish current block + blocks.push(currentBlock); + currentBlock = null; + return; + } + + if (!line) return; // Skip empty lines when no current block + + // Check for headings + if (line.match(/^#{1,6}\s/)) { + if (currentBlock) blocks.push(currentBlock); + const level = line.match(/^#+/)[0].length; + currentBlock = { + type: `h${level}`, + content: line.replace(/^#+\s*/, '').trim() + }; + } + // Check for links (potential buttons) + else if (line.match(/\[([^\]]+)\]\(([^)]+)\)/)) { + if (currentBlock) blocks.push(currentBlock); + const match = line.match(/\[([^\]]+)\]\(([^)]+)\)/); + currentBlock = { + type: 'link', + content: match[1], + href: match[2] + }; + } + // Regular paragraph text + else { + if (!currentBlock) { + currentBlock = { type: 'p', content: line }; + } else if (currentBlock.type === 'p') { + currentBlock.content += ' ' + line; + } else { + // Different block type, finish current and start new + blocks.push(currentBlock); + currentBlock = { type: 'p', content: line }; + } + } + }); + + if (currentBlock) blocks.push(currentBlock); + return blocks; + } + + canUpdateElement(element, blockType) { + const tagName = element.tagName.toLowerCase(); + + // Check if element type matches block type + if (tagName === blockType) return true; + if (tagName === 'a' && blockType === 'link') return true; + if (tagName === 'p' && blockType === 'p') return true; + + return false; + } + + updateElementContent(element, block) { + if (block.type === 'link' && element.tagName.toLowerCase() === 'a') { + // Update link while preserving classes (like btn-primary) + element.textContent = block.content; + element.href = block.href; + } else { + // Update text content while preserving all attributes and classes + element.textContent = block.content; + } + } + + createElementFromBlock(block) { + let element; + + switch (block.type) { + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + element = document.createElement(block.type); + element.textContent = block.content; + break; + + case 'link': + element = document.createElement('a'); + element.textContent = block.content; + element.href = block.href; + // Try to detect if this should be a button + if (this.looksLikeButton(block.content)) { + element.className = 'btn-primary'; + } + break; + + case 'p': + default: + element = document.createElement('p'); + element.textContent = block.content; + break; + } + + return element; + } + + looksLikeButton(text) { + // Heuristics to detect button-like text + const buttonKeywords = [ + 'get started', 'start', 'begin', 'click here', 'learn more', + 'contact', 'call', 'schedule', 'book', 'download', 'sign up', + 'register', 'join', 'subscribe', 'buy', 'order', 'purchase' + ]; + + const lowerText = text.toLowerCase(); + return buttonKeywords.some(keyword => lowerText.includes(keyword)); + } + cancelEditing(contentId) { const element = this.editablElements.get(contentId); if (!element) return;