Implement element-level editing with semantic field detection

Replace container-based blob editing approach with individual element editing. Each .insertr element now gets appropriate field type (text, textarea, link) based on HTML tag and classes. Provides much better UX with separate inputs for headlines, paragraphs, and buttons while preserving HTML structure and styling.
This commit is contained in:
2025-08-30 17:01:25 +02:00
parent e8c0de233d
commit a13546aac2
3 changed files with 288 additions and 409 deletions

View File

@@ -11,9 +11,7 @@
<!-- Navigation --> <!-- Navigation -->
<nav class="navbar"> <nav class="navbar">
<div class="container"> <div class="container">
<div class="insertr" data-content-id="nav-logo"> <h1 class="logo insertr" data-content-id="nav-logo">Acme Consulting</h1>
<h1 class="logo">Acme Consulting</h1>
</div>
<ul class="nav-links"> <ul class="nav-links">
<li><a href="index.html">Home</a></li> <li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li> <li><a href="about.html">About</a></li>
@@ -30,40 +28,30 @@
<!-- Hero Section --> <!-- Hero Section -->
<section class="hero"> <section class="hero">
<div class="container"> <div class="container">
<div class="insertr" data-content-id="hero-content" data-content-type="rich"> <h1 class="insertr" data-content-id="hero-headline">Transform Your Business with Expert Consulting</h1>
<h1>Transform Your Business with Expert Consulting</h1> <p class="lead insertr" data-content-id="hero-description">We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.</p>
<p class="lead">We help small businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.</p> <a href="contact.html" class="btn-primary insertr" data-content-id="hero-cta">Get Started Today</a>
<a href="contact.html" class="btn-primary">Get Started Today</a>
</div>
</div> </div>
</section> </section>
<!-- Services Section --> <!-- Services Section -->
<section class="services"> <section class="services">
<div class="container"> <div class="container">
<div class="insertr" data-content-id="services-title"> <h2 class="insertr" data-content-id="services-title">Our Services</h2>
<h2>Our Services</h2> <p class="section-subtitle insertr" data-content-id="services-subtitle">Comprehensive solutions tailored to your business needs</p>
<p class="section-subtitle">Comprehensive solutions tailored to your business needs</p>
</div>
<div class="services-grid"> <div class="services-grid">
<div class="service-card"> <div class="service-card">
<div class="insertr" data-content-id="service-strategy" data-content-type="rich"> <h3 class="insertr" data-content-id="service-strategy-title">Strategic Planning</h3>
<h3>Strategic Planning</h3> <p class="insertr" data-content-id="service-strategy-desc">Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
<p>Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
</div>
</div> </div>
<div class="service-card"> <div class="service-card">
<div class="insertr" data-content-id="service-operations" data-content-type="rich"> <h3 class="insertr" data-content-id="service-operations-title">Operations Optimization</h3>
<h3>Operations Optimization</h3> <p class="insertr" data-content-id="service-operations-desc">Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
<p>Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
</div>
</div> </div>
<div class="service-card"> <div class="service-card">
<div class="insertr" data-content-id="service-digital" data-content-type="rich"> <h3 class="insertr" data-content-id="service-digital-title">Digital Transformation</h3>
<h3>Digital Transformation</h3> <p class="insertr" data-content-id="service-digital-desc">Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
<p>Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -72,39 +60,31 @@
<!-- Testimonial Section --> <!-- Testimonial Section -->
<section class="testimonial"> <section class="testimonial">
<div class="container"> <div class="container">
<div class="insertr" data-content-id="testimonial-content" data-content-type="rich"> <blockquote>
<blockquote> <p class="insertr" data-content-id="testimonial-quote">"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
<p>"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p> <cite class="insertr" data-content-id="testimonial-author">Sarah Johnson, CEO of TechStart Inc.</cite>
<cite>Sarah Johnson, CEO of TechStart Inc.</cite> </blockquote>
</blockquote>
</div>
</div> </div>
</section> </section>
<!-- Call to Action --> <!-- Call to Action -->
<section class="cta"> <section class="cta">
<div class="container"> <div class="container">
<div class="insertr" data-content-id="cta-content"> <h2 class="insertr" data-content-id="cta-title">Ready to Transform Your Business?</h2>
<h2>Ready to Transform Your Business?</h2> <p class="insertr" data-content-id="cta-description">Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
<p>Contact us today for a free consultation and discover how we can help you achieve your goals.</p> <a href="contact.html" class="btn-primary insertr" data-content-id="cta-button">Schedule Consultation</a>
<a href="contact.html" class="btn-primary">Schedule Consultation</a>
</div>
</div> </div>
</section> </section>
<!-- Footer --> <!-- Footer -->
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">
<div class="insertr" data-content-id="footer-info"> <p class="insertr" data-content-id="footer-copyright">&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p>&copy; 2024 Acme Consulting Services. All rights reserved.</p> <p class="insertr" data-content-id="footer-contact">📧 info@acmeconsulting.com | 📞 (555) 123-4567</p>
<p>📧 info@acmeconsulting.com | 📞 (555) 123-4567</p>
</div>
</div> </div>
</footer> </footer>
<!-- Insertr JavaScript Library --> <!-- Insertr JavaScript Library -->
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="insertr/insertr.js"></script> <script src="insertr/insertr.js"></script>
</body> </body>
</html> </html>

View File

@@ -41,16 +41,33 @@
transform: scale(1.05); transform: scale(1.05);
} }
/* Edit overlay container */
.insertr-edit-overlay {
position: absolute;
z-index: 1000;
min-width: 300px;
}
/* Edit form container */ /* Edit form container */
.insertr-edit-form { .insertr-edit-form {
position: relative;
background: white; background: white;
border: 2px solid #3b82f6; border: 2px solid #3b82f6;
border-radius: 8px; border-radius: 8px;
padding: 1rem; padding: 1rem;
margin: 0.5rem 0; box-shadow: 0 8px 25px rgba(0,0,0,0.15);
box-shadow: 0 4px 12px rgba(0,0,0,0.1); min-width: 280px;
z-index: 20; }
/* Form header */
.insertr-form-header {
font-weight: 600;
color: #1f2937;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e5e7eb;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
} }
/* Form controls */ /* Form controls */

View File

@@ -1,6 +1,6 @@
/** /**
* Insertr - Edit-in-place CMS Library * Insertr - Element-Level Edit-in-place CMS Library
* Simple integration: add class="insertr" to any element * Add class="insertr" to any element to make it editable
*/ */
class Insertr { class Insertr {
@@ -17,73 +17,37 @@ class Insertr {
isAuthenticated: false, isAuthenticated: false,
editMode: false, editMode: false,
currentUser: null, currentUser: null,
contentCache: new Map() contentCache: new Map(),
activeEditor: null
}; };
this.editablElements = new Map(); // Field type detection mapping
this.fieldTypeMap = {
'H1': { type: 'text', label: 'Headline', maxLength: 60, placeholder: 'Enter headline...' },
'H2': { type: 'text', label: 'Subheading', maxLength: 80, placeholder: 'Enter subheading...' },
'H3': { type: 'text', label: 'Section Title', maxLength: 100, placeholder: 'Enter title...' },
'H4': { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
'H5': { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
'H6': { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
'P': { type: 'textarea', label: 'Paragraph', rows: 3, placeholder: 'Enter paragraph text...' },
'A': { type: 'link', label: 'Link', placeholder: 'Enter link text...' },
'SPAN': { type: 'text', label: 'Text', placeholder: 'Enter text...' },
'CITE': { type: 'text', label: 'Citation', placeholder: 'Enter citation...' },
'BUTTON': { type: 'text', label: 'Button Text', placeholder: 'Enter button text...' }
};
this.editableElements = new Map();
this.statusIndicator = null; this.statusIndicator = null;
if (this.options.autoInit) { if (this.options.autoInit) {
this.init(); this.init();
} }
// Initialize marked.js with custom renderer
this.initializeMarkdown();
}
initializeMarkdown() {
// Check if marked is available in different ways it might be exposed
if (typeof marked === 'undefined' && typeof window.marked === 'undefined') {
console.error('Marked.js not loaded! Please include marked.js before insertr.js');
return;
}
// Get the marked object (UMD build exposes it as window.marked)
this.marked = window.marked || marked;
if (!this.marked || typeof this.marked !== 'object') {
console.error('Cannot find marked object');
return;
}
// Get the marked function from the object
this.markedParser = this.marked.marked || this.marked.parse || this.marked;
if (typeof this.markedParser !== 'function') {
console.error('Cannot find marked parse function', typeof this.markedParser);
return;
}
console.log('✅ Marked.js loaded successfully', this.marked);
// For marked v16.x, we configure using the .use() method on the marked object
if (this.marked.use) {
this.marked.use({
renderer: {
link: (href, title, text) => {
const isButton = this.looksLikeButton(text);
const className = isButton ? ' class="btn-primary"' : '';
const titleAttr = title ? ` title="${title}"` : '';
return `<a href="${href}"${className}${titleAttr}>${text}</a>`;
},
paragraph: (text) => {
// Check if this should be a lead paragraph based on content/context
const isLead = this.isLeadParagraph(text);
const className = isLead ? ' class="lead"' : '';
return `<p${className}>${text}</p>`;
}
},
breaks: true,
gfm: true
});
}
} }
async init() { async init() {
console.log('🚀 Insertr initializing...'); console.log('🚀 Insertr initializing with element-level editing...');
// Load content from localStorage (mock persistence) // Load content from localStorage
this.loadContentFromStorage(); this.loadContentFromStorage();
// Scan for editable elements // Scan for editable elements
@@ -98,7 +62,7 @@ class Insertr {
// Apply initial state // Apply initial state
this.updateBodyClasses(); this.updateBodyClasses();
console.log(`📝 Found ${this.editablElements.size} editable elements`); console.log(`📝 Found ${this.editableElements.size} editable elements`);
} }
scanForEditableElements() { scanForEditableElements() {
@@ -112,37 +76,20 @@ class Insertr {
} }
// Store reference and setup // Store reference and setup
this.editablElements.set(contentId, element); this.editableElements.set(contentId, element);
this.setupEditableElement(element, contentId); this.setupEditableElement(element, contentId);
}); });
} }
setupEditableElement(element, contentId) { setupEditableElement(element, contentId) {
// Store original clean content before adding edit interface // Generate field configuration for this element
if (!element.querySelector('.insertr-original-content')) { const fieldConfig = this.generateFieldConfig(element);
const originalContent = element.cloneNode(true);
originalContent.classList.add('insertr-original-content');
originalContent.style.display = 'none';
element.appendChild(originalContent);
}
// Remove any existing edit buttons to avoid duplicates // Store field config on element
const existingBtn = element.querySelector('.insertr-edit-btn'); element._insertrConfig = fieldConfig;
if (existingBtn) existingBtn.remove();
// Add edit button // Add edit button (hidden by default)
const editBtn = document.createElement('button'); this.addEditButton(element, contentId);
editBtn.className = 'insertr-edit-btn';
editBtn.innerHTML = '✏️';
editBtn.title = 'Edit content';
editBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.startEditing(contentId);
});
element.style.position = 'relative';
element.appendChild(editBtn);
// Load saved content if available // Load saved content if available
const savedContent = this.state.contentCache.get(contentId); const savedContent = this.state.contentCache.get(contentId);
@@ -151,45 +98,128 @@ class Insertr {
} }
} }
startEditing(contentId) { generateFieldConfig(element) {
const element = this.editablElements.get(contentId); const tagName = element.tagName;
if (!element) return; let config = { ...this.fieldTypeMap[tagName] } || { type: 'text', label: 'Content', placeholder: 'Enter content...' };
const contentType = element.getAttribute('data-content-type') || 'simple'; // Enhance based on classes and context
const currentContent = this.extractContentFromElement(element); if (element.classList.contains('lead')) {
config.label = 'Lead Paragraph';
config.rows = 4;
config.placeholder = 'Enter lead paragraph...';
}
// Create edit form if (element.classList.contains('btn-primary') || element.classList.contains('btn-secondary')) {
const form = this.createEditForm(contentId, contentType, currentContent); config.type = 'link';
config.label = 'Button';
config.includeUrl = true;
config.placeholder = 'Enter button text...';
}
// Hide original content and show form if (element.classList.contains('section-subtitle')) {
const originalContent = element.querySelector('.insertr-original-content') || element.cloneNode(true); config.label = 'Section Subtitle';
originalContent.classList.add('insertr-original-content'); config.placeholder = 'Enter subtitle...';
originalContent.style.display = 'none'; }
// Clear element and add form // Special handling for certain content IDs
element.innerHTML = ''; const contentId = element.getAttribute('data-content-id');
element.appendChild(originalContent); if (contentId && contentId.includes('cta')) {
element.appendChild(form); config.label = 'Call to Action';
}
if (contentId && contentId.includes('quote')) {
config.type = 'textarea';
config.rows = 3;
config.label = 'Quote';
config.placeholder = 'Enter quote...';
}
return config;
} }
createEditForm(contentId, contentType, currentContent) { addEditButton(element, contentId) {
// Remove existing edit button if any
const existingBtn = element.querySelector('.insertr-edit-btn');
if (existingBtn) existingBtn.remove();
// Create edit button
const editBtn = document.createElement('button');
editBtn.className = 'insertr-edit-btn';
editBtn.innerHTML = '✏️';
editBtn.title = `Edit ${element._insertrConfig.label}`;
editBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.startEditing(contentId);
});
// Position relative for button placement
if (getComputedStyle(element).position === 'static') {
element.style.position = 'relative';
}
element.appendChild(editBtn);
}
startEditing(contentId) {
const element = this.editableElements.get(contentId);
if (!element || !this.state.editMode) return;
// Close any active editor
if (this.state.activeEditor && this.state.activeEditor !== contentId) {
this.cancelEditing(this.state.activeEditor);
}
const config = element._insertrConfig;
const currentContent = this.extractContentFromElement(element);
// Create and show edit form
const form = this.createEditForm(contentId, config, currentContent);
this.showEditForm(element, form);
this.state.activeEditor = contentId;
}
createEditForm(contentId, config, currentContent) {
const form = document.createElement('div'); const form = document.createElement('div');
form.className = 'insertr-edit-form'; form.className = 'insertr-edit-form';
let formHTML = ''; let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
if (contentType === 'rich') { if (config.type === 'link' && config.includeUrl) {
formHTML = ` // Special handling for links - text and URL fields
const element = this.editableElements.get(contentId);
const currentUrl = element.href || '';
formHTML += `
<div class="insertr-form-group"> <div class="insertr-form-group">
<label class="insertr-form-label">Content (Markdown)</label> <label class="insertr-form-label">Link Text</label>
<textarea class="insertr-form-textarea" name="content" placeholder="Enter content in Markdown format...">${this.htmlToMarkdown(currentContent)}</textarea> <input type="text" class="insertr-form-input" name="text"
value="${this.escapeHtml(currentContent)}"
placeholder="${config.placeholder}">
</div>
<div class="insertr-form-group">
<label class="insertr-form-label">Link URL</label>
<input type="url" class="insertr-form-input" name="url"
value="${this.escapeHtml(currentUrl)}"
placeholder="https://example.com">
</div>
`;
} else if (config.type === 'textarea') {
formHTML += `
<div class="insertr-form-group">
<textarea class="insertr-form-textarea" name="content"
rows="${config.rows || 3}"
placeholder="${config.placeholder}">${this.escapeHtml(currentContent)}</textarea>
</div> </div>
`; `;
} else { } else {
formHTML = ` formHTML += `
<div class="insertr-form-group"> <div class="insertr-form-group">
<label class="insertr-form-label">Content</label> <input type="text" class="insertr-form-input" name="content"
<input type="text" class="insertr-form-input" name="content" value="${currentContent.replace(/"/g, '&quot;')}" placeholder="Enter content..."> value="${this.escapeHtml(currentContent)}"
placeholder="${config.placeholder}"
maxlength="${config.maxLength || ''}">
</div> </div>
`; `;
} }
@@ -209,32 +239,74 @@ class Insertr {
}); });
form.querySelector('.insertr-btn-save').addEventListener('click', () => { form.querySelector('.insertr-btn-save').addEventListener('click', () => {
const input = form.querySelector('input, textarea'); this.saveElementContent(contentId, form);
this.saveContent(contentId, input.value);
}); });
// Focus on input // Focus on first input
setTimeout(() => { setTimeout(() => {
const input = form.querySelector('input, textarea'); const firstInput = form.querySelector('input, textarea');
input.focus(); if (firstInput) {
if (input.type === 'text') { firstInput.focus();
input.select(); if (firstInput.type === 'text' || firstInput.tagName === 'TEXTAREA') {
firstInput.select();
}
} }
}, 100); }, 100);
return form; return form;
} }
async saveContent(contentId, newContent) { showEditForm(element, form) {
const element = this.editablElements.get(contentId); // Hide edit button during editing
const editBtn = element.querySelector('.insertr-edit-btn');
if (editBtn) editBtn.style.display = 'none';
// Create overlay container
const overlay = document.createElement('div');
overlay.className = 'insertr-edit-overlay';
overlay.appendChild(form);
// Position overlay near element
document.body.appendChild(overlay);
this.positionEditForm(element, overlay);
// Store reference for cleanup
element._insertrOverlay = overlay;
}
positionEditForm(element, overlay) {
const rect = element.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
overlay.style.position = 'absolute';
overlay.style.top = `${rect.bottom + scrollTop + 10}px`;
overlay.style.left = `${rect.left + scrollLeft}px`;
overlay.style.zIndex = '1000';
overlay.style.minWidth = `${Math.min(300, rect.width)}px`;
}
async saveElementContent(contentId, form) {
const element = this.editableElements.get(contentId);
if (!element) return; if (!element) return;
const config = element._insertrConfig;
let newContent = {};
// Extract form data based on field type
if (config.type === 'link' && config.includeUrl) {
newContent.text = form.querySelector('input[name="text"]').value;
newContent.url = form.querySelector('input[name="url"]').value;
} else {
const input = form.querySelector('input[name="content"], textarea[name="content"]');
newContent.text = input.value;
}
// Add saving state // Add saving state
element.classList.add('insertr-saving'); element.classList.add('insertr-saving');
try { try {
// In a real implementation, this would make an API call // Simulate save
// For now, we'll simulate it and store in localStorage
await this.simulateSave(contentId, newContent); await this.simulateSave(contentId, newContent);
// Update cache // Update cache
@@ -242,7 +314,10 @@ class Insertr {
this.saveContentToStorage(); this.saveContentToStorage();
// Apply new content to element // Apply new content to element
this.applyContentToElement(element, newContent, true); this.applyContentToElement(element, newContent);
// Close editor
this.cancelEditing(contentId);
// Show success state // Show success state
element.classList.add('insertr-save-success'); element.classList.add('insertr-save-success');
@@ -258,203 +333,58 @@ class Insertr {
} }
} }
applyContentToElement(element, content, isEdit = false) { applyContentToElement(element, content) {
const contentType = element.getAttribute('data-content-type') || 'simple'; const config = element._insertrConfig;
let originalContent = element.querySelector('.insertr-original-content');
if (!originalContent) { if (config.type === 'link' && config.includeUrl && content.url !== undefined) {
// First time, clone current content // Update link text and URL
originalContent = element.cloneNode(true); element.textContent = content.text || element.textContent;
originalContent.classList.add('insertr-original-content'); if (content.url) {
} element.href = content.url;
// Update content based on type
if (contentType === 'rich') {
// For rich content, intelligently update existing HTML structure
this.updateRichContent(originalContent, content);
} else {
// For simple content, update text while preserving structure
this.updateSimpleContent(originalContent, content);
}
// Replace element content
element.innerHTML = '';
originalContent.style.display = '';
element.appendChild(originalContent);
// Re-setup edit functionality
this.setupEditableElement(element, element.getAttribute('data-content-id'));
}
updateRichContent(container, markdownContent) {
// Use marked.js to convert markdown to HTML with our custom renderer
if (!this.markedParser) {
console.error('Marked parser not initialized');
return;
}
const html = this.markedParser(markdownContent);
// Create temporary container to parse the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Get existing elements (excluding edit button)
const existingElements = Array.from(container.children).filter(el =>
!el.classList.contains('insertr-edit-btn')
);
// Get new elements from marked output
const newElements = Array.from(tempDiv.children);
// Smart merge: preserve existing elements where possible, add new ones
newElements.forEach((newEl, index) => {
const existingEl = existingElements[index];
if (existingEl && existingEl.tagName === newEl.tagName) {
// Same element type - preserve classes and update content
this.mergeElementContent(existingEl, newEl);
} else {
// Different type or new element - replace or add
if (existingEl) {
container.replaceChild(newEl, existingEl);
} else {
container.appendChild(newEl);
}
}
});
// Remove any extra existing elements
for (let i = newElements.length; i < existingElements.length; i++) {
if (existingElements[i]) {
container.removeChild(existingElements[i]);
} }
} else if (content.text !== undefined) {
// Update text content
element.textContent = content.text;
} }
} }
mergeElementContent(existingEl, newEl) {
// Preserve existing classes while updating content
const existingClasses = existingEl.className;
const newClasses = newEl.className;
// Combine classes (existing takes precedence)
if (existingClasses) {
existingEl.className = existingClasses;
// Add any new important classes
if (newClasses && !existingClasses.includes(newClasses)) {
existingEl.className += ' ' + newClasses;
}
} else {
existingEl.className = newClasses;
}
// Update content and attributes
existingEl.innerHTML = newEl.innerHTML;
// Preserve/update href for links
if (newEl.href) {
existingEl.href = newEl.href;
}
// Preserve/update title
if (newEl.title) {
existingEl.title = newEl.title;
}
}
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;
}
}
}
// Helper method to detect if a paragraph should have 'lead' class
isLeadParagraph(text) {
// Heuristics for lead paragraphs:
// - Usually the first substantial paragraph
// - Often longer than average
// - Contains descriptive/intro language
return text.length > 100 && (
text.toLowerCase().includes('we help') ||
text.toLowerCase().includes('our team') ||
text.toLowerCase().includes('experience') ||
text.toLowerCase().includes('business') ||
text.toLowerCase().includes('services')
);
}
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) { cancelEditing(contentId) {
const element = this.editablElements.get(contentId); const element = this.editableElements.get(contentId);
if (!element) return; if (!element) return;
const originalContent = element.querySelector('.insertr-original-content'); // Remove overlay
if (originalContent) { const overlay = element._insertrOverlay;
element.innerHTML = ''; if (overlay && overlay.parentNode) {
originalContent.style.display = ''; overlay.parentNode.removeChild(overlay);
element.appendChild(originalContent);
this.setupEditableElement(element, contentId);
} }
// Show edit button again
const editBtn = element.querySelector('.insertr-edit-btn');
if (editBtn) editBtn.style.display = '';
// Clear active editor
if (this.state.activeEditor === contentId) {
this.state.activeEditor = null;
}
delete element._insertrOverlay;
} }
extractContentFromElement(element) { extractContentFromElement(element) {
// Get the original content without edit interface elements // Clone element to avoid modifying original
const originalContent = element.querySelector('.insertr-original-content'); const clone = element.cloneNode(true);
if (originalContent) { // Remove edit button from clone
// We have the original content stored, use that const editBtn = clone.querySelector('.insertr-edit-btn');
const clone = originalContent.cloneNode(true); if (editBtn) {
// Remove any edit buttons from the clone editBtn.remove();
const editBtns = clone.querySelectorAll('.insertr-edit-btn');
editBtns.forEach(btn => btn.remove());
const contentType = element.getAttribute('data-content-type') || 'simple';
if (contentType === 'rich') {
// For rich content, convert back to markdown
return this.htmlToMarkdown(clone.innerHTML);
} else {
// For simple content, return clean text
return clone.textContent.trim();
}
} else {
// Fallback: extract from current element (first time editing)
const clone = element.cloneNode(true);
// Remove edit button and any insertr interface elements
const editBtns = clone.querySelectorAll('.insertr-edit-btn');
editBtns.forEach(btn => btn.remove());
const contentType = element.getAttribute('data-content-type') || 'simple';
if (contentType === 'rich') {
// For rich content, convert HTML to markdown
return this.htmlToMarkdown(clone.innerHTML);
} else {
// For simple content, return clean text
return clone.textContent.trim();
}
} }
// Extract clean text content
return clone.textContent.trim();
} }
// Authentication and UI methods (unchanged from previous version)
setupAuthenticationControls() { setupAuthenticationControls() {
const authToggle = document.getElementById('auth-toggle'); const authToggle = document.getElementById('auth-toggle');
const editModeToggle = document.getElementById('edit-mode-toggle'); const editModeToggle = document.getElementById('edit-mode-toggle');
@@ -489,6 +419,11 @@ class Insertr {
editModeToggle.style.display = 'none'; editModeToggle.style.display = 'none';
this.state.editMode = false; this.state.editMode = false;
this.state.currentUser = null; this.state.currentUser = null;
// Close any active editor
if (this.state.activeEditor) {
this.cancelEditing(this.state.activeEditor);
}
} }
this.updateBodyClasses(); this.updateBodyClasses();
@@ -498,6 +433,11 @@ class Insertr {
toggleEditMode() { toggleEditMode() {
if (!this.state.isAuthenticated) return; if (!this.state.isAuthenticated) return;
// Close any active editor when toggling edit mode
if (this.state.activeEditor) {
this.cancelEditing(this.state.activeEditor);
}
this.state.editMode = !this.state.editMode; this.state.editMode = !this.state.editMode;
const editModeToggle = document.getElementById('edit-mode-toggle'); const editModeToggle = document.getElementById('edit-mode-toggle');
@@ -542,72 +482,14 @@ class Insertr {
this.statusIndicator.className = className; this.statusIndicator.className = className;
} }
// Utility methods for content conversion // Utility methods
// Note: markdownToHtml is now handled by marked.js in updateRichContent escapeHtml(text) {
const div = document.createElement('div');
htmlToMarkdown(html) { div.textContent = text;
// Simple HTML to markdown conversion return div.innerHTML;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Remove edit buttons and other insertr elements
tempDiv.querySelectorAll('.insertr-edit-btn').forEach(btn => btn.remove());
// Convert HTML back to markdown
let markdown = tempDiv.innerHTML;
// Convert headings
markdown = markdown.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n');
markdown = markdown.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n');
markdown = markdown.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n');
// Convert formatting
markdown = markdown.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**');
markdown = markdown.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*');
// Convert links
markdown = markdown.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)');
// Convert lists
markdown = markdown.replace(/<ul[^>]*>(.*?)<\/ul>/gis, (match, content) => {
return content.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n');
});
// Convert paragraphs and preserve structure
markdown = markdown.replace(/<p[^>]*class="lead"[^>]*>(.*?)<\/p>/gi, '\n$1\n\n');
markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gi, '\n$1\n\n');
// Clean up HTML tags and entities
markdown = markdown.replace(/<[^>]*>/g, '');
markdown = markdown.replace(/&nbsp;/g, ' ');
markdown = markdown.replace(/&amp;/g, '&');
markdown = markdown.replace(/&lt;/g, '<');
markdown = markdown.replace(/&gt;/g, '>');
// Clean up excessive whitespace and normalize line breaks
markdown = markdown.replace(/[ \t]+/g, ' '); // Multiple spaces/tabs to single space
markdown = markdown.replace(/\n[ \t]+/g, '\n'); // Remove leading whitespace on lines
markdown = markdown.replace(/[ \t]+\n/g, '\n'); // Remove trailing whitespace on lines
markdown = markdown.replace(/\n\n\n+/g, '\n\n'); // Multiple blank lines to double
markdown = markdown.replace(/^\n+/, ''); // Remove leading newlines
markdown = markdown.replace(/\n+$/, ''); // Remove trailing newlines
return markdown.trim();
} }
getTextNodes(node) { // Storage methods
const textNodes = [];
if (node.nodeType === Node.TEXT_NODE) {
textNodes.push(node);
} else {
for (let child of node.childNodes) {
textNodes.push(...this.getTextNodes(child));
}
}
return textNodes;
}
// Storage methods (mock persistence)
loadContentFromStorage() { loadContentFromStorage() {
try { try {
const stored = localStorage.getItem(this.options.storageKey); const stored = localStorage.getItem(this.options.storageKey);