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:
@@ -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>
|
||||
<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>
|
||||
</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>© 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">© 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>
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
// Create edit form
|
||||
const form = this.createEditForm(contentId, contentType, currentContent);
|
||||
|
||||
// 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';
|
||||
|
||||
// Clear element and add form
|
||||
element.innerHTML = '';
|
||||
element.appendChild(originalContent);
|
||||
element.appendChild(form);
|
||||
// Enhance based on classes and context
|
||||
if (element.classList.contains('lead')) {
|
||||
config.label = 'Lead Paragraph';
|
||||
config.rows = 4;
|
||||
config.placeholder = 'Enter lead paragraph...';
|
||||
}
|
||||
|
||||
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...';
|
||||
}
|
||||
|
||||
if (element.classList.contains('section-subtitle')) {
|
||||
config.label = 'Section Subtitle';
|
||||
config.placeholder = 'Enter subtitle...';
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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, '"')}" 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');
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
} else if (content.text !== undefined) {
|
||||
// Update text content
|
||||
element.textContent = content.text;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
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)
|
||||
// Clone element to avoid modifying original
|
||||
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';
|
||||
// Remove edit button from clone
|
||||
const editBtn = clone.querySelector('.insertr-edit-btn');
|
||||
if (editBtn) {
|
||||
editBtn.remove();
|
||||
}
|
||||
|
||||
if (contentType === 'rich') {
|
||||
// For rich content, convert HTML to markdown
|
||||
return this.htmlToMarkdown(clone.innerHTML);
|
||||
} else {
|
||||
// For simple content, return clean text
|
||||
// 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(/ /g, ' ');
|
||||
markdown = markdown.replace(/&/g, '&');
|
||||
markdown = markdown.replace(/</g, '<');
|
||||
markdown = markdown.replace(/>/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);
|
||||
|
||||
Reference in New Issue
Block a user