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 -->
|
<!-- 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>"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
|
<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>Sarah Johnson, CEO of TechStart Inc.</cite>
|
<cite class="insertr" data-content-id="testimonial-author">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">© 2024 Acme Consulting Services. All rights reserved.</p>
|
||||||
<p>© 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>
|
||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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';
|
||||||
// Create edit form
|
config.rows = 4;
|
||||||
const form = this.createEditForm(contentId, contentType, currentContent);
|
config.placeholder = 'Enter lead paragraph...';
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
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, '"')}" 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;
|
||||||
}
|
}
|
||||||
|
} else if (content.text !== undefined) {
|
||||||
// Update content based on type
|
// Update text content
|
||||||
if (contentType === 'rich') {
|
element.textContent = content.text;
|
||||||
// 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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
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);
|
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') {
|
// Extract clean text content
|
||||||
// For rich content, convert HTML to markdown
|
|
||||||
return this.htmlToMarkdown(clone.innerHTML);
|
|
||||||
} else {
|
|
||||||
// For simple content, return clean text
|
|
||||||
return clone.textContent.trim();
|
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(/ /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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user