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 -->
<nav class="navbar">
<div class="container">
<div class="insertr" data-content-id="nav-logo">
<h1 class="logo">Acme Consulting</h1>
</div>
<h1 class="logo insertr" data-content-id="nav-logo">Acme Consulting</h1>
<ul class="nav-links">
<li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li>
@@ -30,40 +28,30 @@
<!-- Hero Section -->
<section class="hero">
<div class="container">
<div class="insertr" data-content-id="hero-content" data-content-type="rich">
<h1>Transform Your Business with Expert Consulting</h1>
<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">Get Started Today</a>
</div>
<h1 class="insertr" data-content-id="hero-headline">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>
<a href="contact.html" class="btn-primary insertr" data-content-id="hero-cta">Get Started Today</a>
</div>
</section>
<!-- Services Section -->
<section class="services">
<div class="container">
<div class="insertr" data-content-id="services-title">
<h2>Our Services</h2>
<p class="section-subtitle">Comprehensive solutions tailored to your business needs</p>
</div>
<h2 class="insertr" data-content-id="services-title">Our Services</h2>
<p class="section-subtitle insertr" data-content-id="services-subtitle">Comprehensive solutions tailored to your business needs</p>
<div class="services-grid">
<div class="service-card">
<div class="insertr" data-content-id="service-strategy" data-content-type="rich">
<h3>Strategic Planning</h3>
<p>Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
</div>
<h3 class="insertr" data-content-id="service-strategy-title">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>
</div>
<div class="service-card">
<div class="insertr" data-content-id="service-operations" data-content-type="rich">
<h3>Operations Optimization</h3>
<p>Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
</div>
<h3 class="insertr" data-content-id="service-operations-title">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>
</div>
<div class="service-card">
<div class="insertr" data-content-id="service-digital" data-content-type="rich">
<h3>Digital Transformation</h3>
<p>Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
</div>
<h3 class="insertr" data-content-id="service-digital-title">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>
</div>
</div>
</div>
@@ -72,39 +60,31 @@
<!-- Testimonial Section -->
<section class="testimonial">
<div class="container">
<div class="insertr" data-content-id="testimonial-content" data-content-type="rich">
<blockquote>
<p>"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
<cite>Sarah Johnson, CEO of TechStart Inc.</cite>
</blockquote>
</div>
<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>
<cite class="insertr" data-content-id="testimonial-author">Sarah Johnson, CEO of TechStart Inc.</cite>
</blockquote>
</div>
</section>
<!-- Call to Action -->
<section class="cta">
<div class="container">
<div class="insertr" data-content-id="cta-content">
<h2>Ready to Transform Your Business?</h2>
<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">Schedule Consultation</a>
</div>
<h2 class="insertr" data-content-id="cta-title">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>
<a href="contact.html" class="btn-primary insertr" data-content-id="cta-button">Schedule Consultation</a>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="insertr" data-content-id="footer-info">
<p>&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p>📧 info@acmeconsulting.com | 📞 (555) 123-4567</p>
</div>
<p class="insertr" data-content-id="footer-copyright">&copy; 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr" data-content-id="footer-contact">📧 info@acmeconsulting.com | 📞 (555) 123-4567</p>
</div>
</footer>
<!-- 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>
</body>
</html>

View File

@@ -41,16 +41,33 @@
transform: scale(1.05);
}
/* Edit overlay container */
.insertr-edit-overlay {
position: absolute;
z-index: 1000;
min-width: 300px;
}
/* Edit form container */
.insertr-edit-form {
position: relative;
background: white;
border: 2px solid #3b82f6;
border-radius: 8px;
padding: 1rem;
margin: 0.5rem 0;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
z-index: 20;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
min-width: 280px;
}
/* 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 */

View File

@@ -1,6 +1,6 @@
/**
* Insertr - Edit-in-place CMS Library
* Simple integration: add class="insertr" to any element
* Insertr - Element-Level Edit-in-place CMS Library
* Add class="insertr" to any element to make it editable
*/
class Insertr {
@@ -17,73 +17,37 @@ class Insertr {
isAuthenticated: false,
editMode: false,
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;
if (this.options.autoInit) {
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() {
console.log('🚀 Insertr initializing...');
console.log('🚀 Insertr initializing with element-level editing...');
// Load content from localStorage (mock persistence)
// Load content from localStorage
this.loadContentFromStorage();
// Scan for editable elements
@@ -98,7 +62,7 @@ class Insertr {
// Apply initial state
this.updateBodyClasses();
console.log(`📝 Found ${this.editablElements.size} editable elements`);
console.log(`📝 Found ${this.editableElements.size} editable elements`);
}
scanForEditableElements() {
@@ -112,37 +76,20 @@ class Insertr {
}
// Store reference and setup
this.editablElements.set(contentId, element);
this.editableElements.set(contentId, element);
this.setupEditableElement(element, contentId);
});
}
setupEditableElement(element, contentId) {
// Store original clean content before adding edit interface
if (!element.querySelector('.insertr-original-content')) {
const originalContent = element.cloneNode(true);
originalContent.classList.add('insertr-original-content');
originalContent.style.display = 'none';
element.appendChild(originalContent);
}
// Generate field configuration for this element
const fieldConfig = this.generateFieldConfig(element);
// Remove any existing edit buttons to avoid duplicates
const existingBtn = element.querySelector('.insertr-edit-btn');
if (existingBtn) existingBtn.remove();
// Store field config on element
element._insertrConfig = fieldConfig;
// Add edit button
const editBtn = document.createElement('button');
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);
// Add edit button (hidden by default)
this.addEditButton(element, contentId);
// Load saved content if available
const savedContent = this.state.contentCache.get(contentId);
@@ -151,45 +98,128 @@ class Insertr {
}
}
startEditing(contentId) {
const element = this.editablElements.get(contentId);
if (!element) return;
generateFieldConfig(element) {
const tagName = element.tagName;
let config = { ...this.fieldTypeMap[tagName] } || { type: 'text', label: 'Content', placeholder: 'Enter content...' };
const contentType = element.getAttribute('data-content-type') || 'simple';
const currentContent = this.extractContentFromElement(element);
// Enhance based on classes and context
if (element.classList.contains('lead')) {
config.label = 'Lead Paragraph';
config.rows = 4;
config.placeholder = 'Enter lead paragraph...';
}
// Create edit form
const form = this.createEditForm(contentId, contentType, currentContent);
if (element.classList.contains('btn-primary') || element.classList.contains('btn-secondary')) {
config.type = 'link';
config.label = 'Button';
config.includeUrl = true;
config.placeholder = 'Enter button text...';
}
// Hide original content and show form
const originalContent = element.querySelector('.insertr-original-content') || element.cloneNode(true);
originalContent.classList.add('insertr-original-content');
originalContent.style.display = 'none';
if (element.classList.contains('section-subtitle')) {
config.label = 'Section Subtitle';
config.placeholder = 'Enter subtitle...';
}
// Clear element and add form
element.innerHTML = '';
element.appendChild(originalContent);
element.appendChild(form);
// Special handling for certain content IDs
const contentId = element.getAttribute('data-content-id');
if (contentId && contentId.includes('cta')) {
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');
form.className = 'insertr-edit-form';
let formHTML = '';
let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
if (contentType === 'rich') {
formHTML = `
if (config.type === 'link' && config.includeUrl) {
// Special handling for links - text and URL fields
const element = this.editableElements.get(contentId);
const currentUrl = element.href || '';
formHTML += `
<div class="insertr-form-group">
<label class="insertr-form-label">Content (Markdown)</label>
<textarea class="insertr-form-textarea" name="content" placeholder="Enter content in Markdown format...">${this.htmlToMarkdown(currentContent)}</textarea>
<label class="insertr-form-label">Link Text</label>
<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>
`;
} else {
formHTML = `
formHTML += `
<div class="insertr-form-group">
<label class="insertr-form-label">Content</label>
<input type="text" class="insertr-form-input" name="content" value="${currentContent.replace(/"/g, '&quot;')}" placeholder="Enter content...">
<input type="text" class="insertr-form-input" name="content"
value="${this.escapeHtml(currentContent)}"
placeholder="${config.placeholder}"
maxlength="${config.maxLength || ''}">
</div>
`;
}
@@ -209,32 +239,74 @@ class Insertr {
});
form.querySelector('.insertr-btn-save').addEventListener('click', () => {
const input = form.querySelector('input, textarea');
this.saveContent(contentId, input.value);
this.saveElementContent(contentId, form);
});
// Focus on input
// Focus on first input
setTimeout(() => {
const input = form.querySelector('input, textarea');
input.focus();
if (input.type === 'text') {
input.select();
const firstInput = form.querySelector('input, textarea');
if (firstInput) {
firstInput.focus();
if (firstInput.type === 'text' || firstInput.tagName === 'TEXTAREA') {
firstInput.select();
}
}
}, 100);
return form;
}
async saveContent(contentId, newContent) {
const element = this.editablElements.get(contentId);
showEditForm(element, form) {
// 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;
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
element.classList.add('insertr-saving');
try {
// In a real implementation, this would make an API call
// For now, we'll simulate it and store in localStorage
// Simulate save
await this.simulateSave(contentId, newContent);
// Update cache
@@ -242,7 +314,10 @@ class Insertr {
this.saveContentToStorage();
// Apply new content to element
this.applyContentToElement(element, newContent, true);
this.applyContentToElement(element, newContent);
// Close editor
this.cancelEditing(contentId);
// Show success state
element.classList.add('insertr-save-success');
@@ -258,203 +333,58 @@ class Insertr {
}
}
applyContentToElement(element, content, isEdit = false) {
const contentType = element.getAttribute('data-content-type') || 'simple';
let originalContent = element.querySelector('.insertr-original-content');
applyContentToElement(element, content) {
const config = element._insertrConfig;
if (!originalContent) {
// First time, clone current content
originalContent = element.cloneNode(true);
originalContent.classList.add('insertr-original-content');
}
// 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]);
if (config.type === 'link' && config.includeUrl && content.url !== undefined) {
// Update link text and URL
element.textContent = content.text || element.textContent;
if (content.url) {
element.href = content.url;
}
} 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) {
const element = this.editablElements.get(contentId);
const element = this.editableElements.get(contentId);
if (!element) return;
const originalContent = element.querySelector('.insertr-original-content');
if (originalContent) {
element.innerHTML = '';
originalContent.style.display = '';
element.appendChild(originalContent);
this.setupEditableElement(element, contentId);
// Remove overlay
const overlay = element._insertrOverlay;
if (overlay && overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
// 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) {
// Get the original content without edit interface elements
const originalContent = element.querySelector('.insertr-original-content');
// Clone element to avoid modifying original
const clone = element.cloneNode(true);
if (originalContent) {
// We have the original content stored, use that
const clone = originalContent.cloneNode(true);
// Remove any edit buttons from the clone
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();
}
// Remove edit button from clone
const editBtn = clone.querySelector('.insertr-edit-btn');
if (editBtn) {
editBtn.remove();
}
// Extract clean text content
return clone.textContent.trim();
}
// Authentication and UI methods (unchanged from previous version)
setupAuthenticationControls() {
const authToggle = document.getElementById('auth-toggle');
const editModeToggle = document.getElementById('edit-mode-toggle');
@@ -489,6 +419,11 @@ class Insertr {
editModeToggle.style.display = 'none';
this.state.editMode = false;
this.state.currentUser = null;
// Close any active editor
if (this.state.activeEditor) {
this.cancelEditing(this.state.activeEditor);
}
}
this.updateBodyClasses();
@@ -498,6 +433,11 @@ class Insertr {
toggleEditMode() {
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;
const editModeToggle = document.getElementById('edit-mode-toggle');
@@ -542,72 +482,14 @@ class Insertr {
this.statusIndicator.className = className;
}
// Utility methods for content conversion
// Note: markdownToHtml is now handled by marked.js in updateRichContent
htmlToMarkdown(html) {
// Simple HTML to markdown conversion
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();
// Utility methods
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
getTextNodes(node) {
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)
// Storage methods
loadContentFromStorage() {
try {
const stored = localStorage.getItem(this.options.storageKey);