Add hybrid editing system with markdown collections

Implement markdown collection support alongside existing element-level editing. Users can now choose between individual element editing (headlines, buttons) and rich markdown editing (stories, bios). Includes responsive overlay forms, smart field detection, and comprehensive documentation updates.

Features:
- Markdown collections with data-field-type="markdown"
- Responsive overlay forms that adapt to element width
- Element-level editing for granular control
- Updated documentation and integration guide
- Fixed marked.js renderer configuration issues
- Enhanced about.html to demonstrate both approaches
This commit is contained in:
2025-09-01 13:20:29 +02:00
parent a13546aac2
commit 39e60e0b3f
5 changed files with 306 additions and 70 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,23 +28,21 @@
<!-- Hero Section -->
<section class="hero">
<div class="container">
<div class="insertr" data-content-id="about-hero" data-content-type="rich">
<h1>About Acme Consulting</h1>
<p class="lead">We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
</div>
<h1 class="insertr" data-content-id="about-title">About Acme Consulting</h1>
<p class="lead insertr" data-content-id="about-subtitle">We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
</div>
</section>
<!-- Story Section -->
<section class="services">
<div class="container">
<div class="insertr" data-content-id="our-story" data-content-type="rich">
<h2>Our Story</h2>
<h2 class="insertr" data-content-id="story-title">Our Story</h2>
<div class="insertr" data-content-id="story-content" data-field-type="markdown">
<p>Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.</p>
<p>Our founders, with combined experience of over 30 years in business strategy, operations, and technology, recognized that the traditional consulting model wasn't serving the needs of growing businesses. We set out to change that.</p>
<p>Today, we've helped over 200 businesses streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
<p>Today, we've helped over **200 businesses** streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
</div>
</div>
</section>
@@ -54,31 +50,29 @@
<!-- Team Section -->
<section class="cta">
<div class="container">
<div class="insertr" data-content-id="team-section">
<h2>Our Team</h2>
<p>We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
</div>
<h2 class="insertr" data-content-id="team-title">Our Team</h2>
<p class="insertr" data-content-id="team-subtitle">We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
<div class="services-grid" style="margin-top: 3rem;">
<div class="service-card">
<div class="insertr" data-content-id="team-member-1" data-content-type="rich">
<div class="insertr" data-content-id="member1-info" data-field-type="markdown">
<h3>Sarah Chen</h3>
<p><strong>Founder & CEO</strong></p>
<p>Former McKinsey consultant with 15 years of experience in strategy and operations. MBA from Stanford.</p>
<p>Former **McKinsey consultant** with 15 years of experience in strategy and operations. MBA from Stanford.</p>
</div>
</div>
<div class="service-card">
<div class="insertr" data-content-id="team-member-2" data-content-type="rich">
<div class="insertr" data-content-id="member2-info" data-field-type="markdown">
<h3>Michael Rodriguez</h3>
<p><strong>Head of Operations</strong></p>
<p>20 years in manufacturing and supply chain optimization. Expert in lean methodologies and process improvement.</p>
<p>20 years in manufacturing and supply chain optimization. Expert in **lean methodologies** and process improvement.</p>
</div>
</div>
<div class="service-card">
<div class="insertr" data-content-id="team-member-3" data-content-type="rich">
<div class="insertr" data-content-id="member3-info" data-field-type="markdown">
<h3>Emma Thompson</h3>
<p><strong>Digital Strategy Lead</strong></p>
<p>Former tech startup founder turned consultant. Specializes in digital transformation and technology adoption.</p>
<p>Former tech startup founder turned consultant. Specializes in *digital transformation* and technology adoption.</p>
</div>
</div>
</div>
@@ -88,21 +82,19 @@
<!-- Values Section -->
<section class="testimonial">
<div class="container">
<div class="insertr" data-content-id="values-section" data-content-type="rich">
<h2 style="margin-bottom: 2rem;">Our Values</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
<div>
<h3>Client-First</h3>
<p>Every recommendation we make is designed with your specific business context and goals in mind.</p>
</div>
<div>
<h3>Practical Solutions</h3>
<p>We believe in strategies that you can actually implement with your current resources and capabilities.</p>
</div>
<div>
<h3>Long-term Partnership</h3>
<p>We're not just consultants; we're partners in your business success for the long haul.</p>
</div>
<h2 class="insertr" data-content-id="values-title" style="margin-bottom: 2rem;">Our Values</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
<div>
<h3 class="insertr" data-content-id="value1-title">Client-First</h3>
<p class="insertr" data-content-id="value1-desc">Every recommendation we make is designed with your specific business context and goals in mind.</p>
</div>
<div>
<h3 class="insertr" data-content-id="value2-title">Practical Solutions</h3>
<p class="insertr" data-content-id="value2-desc">We believe in strategies that you can actually implement with your current resources and capabilities.</p>
</div>
<div>
<h3 class="insertr" data-content-id="value3-title">Long-term Partnership</h3>
<p class="insertr" data-content-id="value3-desc">We're not just consultants; we're partners in your business success for the long haul.</p>
</div>
</div>
</div>
@@ -111,16 +103,13 @@
<!-- 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

@@ -85,6 +85,7 @@
</footer>
<!-- Insertr JavaScript Library -->
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
<script src="insertr/insertr.js"></script>
</body>
</html>

View File

@@ -45,7 +45,6 @@
.insertr-edit-overlay {
position: absolute;
z-index: 1000;
min-width: 300px;
}
/* Edit form container */
@@ -55,7 +54,8 @@
border-radius: 8px;
padding: 1rem;
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
min-width: 280px;
width: 100%;
box-sizing: border-box;
}
/* Form header */
@@ -111,6 +111,14 @@
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
}
.insertr-markdown-editor {
min-height: 200px;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 0.9rem;
line-height: 1.5;
background-color: #f8fafc;
}
/* Form actions */
.insertr-form-actions {
display: flex;

View File

@@ -42,6 +42,36 @@ class Insertr {
if (this.options.autoInit) {
this.init();
}
// Initialize markdown support
this.initializeMarkdown();
}
initializeMarkdown() {
// Check if marked is available
if (typeof marked === 'undefined' && typeof window.marked === 'undefined') {
console.warn('Marked.js not loaded! Markdown collections will not work.');
return;
}
// Get the marked object
this.marked = window.marked || marked;
this.markedParser = this.marked.marked || this.marked.parse || this.marked;
if (typeof this.markedParser !== 'function') {
console.warn('Cannot find marked parse function');
return;
}
console.log('✅ Marked.js loaded successfully');
// Configure marked for basic use - keep it simple for now
if (this.marked.use) {
this.marked.use({
breaks: true,
gfm: true
});
}
}
async init() {
@@ -99,6 +129,17 @@ class Insertr {
}
generateFieldConfig(element) {
// Check for markdown collection type first
const fieldType = element.getAttribute('data-field-type');
if (fieldType === 'markdown') {
return {
type: 'markdown',
label: 'Content (Markdown)',
rows: 8,
placeholder: 'Enter content in Markdown format...\n\nUse **bold**, *italic*, [links](url), and double line breaks for new paragraphs.'
};
}
const tagName = element.tagName;
let config = { ...this.fieldTypeMap[tagName] } || { type: 'text', label: 'Content', placeholder: 'Enter content...' };
@@ -186,7 +227,16 @@ class Insertr {
let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
if (config.type === 'link' && config.includeUrl) {
if (config.type === 'markdown') {
// Markdown collection editing
formHTML += `
<div class="insertr-form-group">
<textarea class="insertr-form-textarea insertr-markdown-editor" name="content"
rows="${config.rows || 8}"
placeholder="${config.placeholder}">${this.escapeHtml(currentContent)}</textarea>
</div>
`;
} else if (config.type === 'link' && config.includeUrl) {
// Special handling for links - text and URL fields
const element = this.editableElements.get(contentId);
const currentUrl = element.href || '';
@@ -283,7 +333,7 @@ class Insertr {
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`;
overlay.style.width = `${Math.max(300, rect.width)}px`;
}
async saveElementContent(contentId, form) {
@@ -294,7 +344,11 @@ class Insertr {
let newContent = {};
// Extract form data based on field type
if (config.type === 'link' && config.includeUrl) {
if (config.type === 'markdown') {
// For markdown, store just the string directly
const input = form.querySelector('textarea[name="content"]');
newContent = input.value;
} else if (config.type === 'link' && config.includeUrl) {
newContent.text = form.querySelector('input[name="text"]').value;
newContent.url = form.querySelector('input[name="url"]').value;
} else {
@@ -336,7 +390,10 @@ class Insertr {
applyContentToElement(element, content) {
const config = element._insertrConfig;
if (config.type === 'link' && config.includeUrl && content.url !== undefined) {
if (config.type === 'markdown') {
// Handle markdown collection - content is a string
this.applyMarkdownContent(element, content);
} else if (config.type === 'link' && config.includeUrl && content.url !== undefined) {
// Update link text and URL
element.textContent = content.text || element.textContent;
if (content.url) {
@@ -348,6 +405,46 @@ class Insertr {
}
}
applyMarkdownContent(element, markdownText) {
if (!this.markedParser) {
console.error('Marked parser not available');
element.textContent = markdownText;
return;
}
// Ensure we have a string
if (typeof markdownText !== 'string') {
console.error('Expected markdown string, got:', typeof markdownText, markdownText);
element.textContent = 'Content type error: ' + typeof markdownText;
return;
}
try {
// Convert markdown to HTML
const html = this.markedParser(markdownText);
if (typeof html !== 'string') {
console.error('Marked parser returned non-string:', typeof html, html);
element.textContent = 'Markdown parsing error';
return;
}
// Store original edit button
const editBtn = element.querySelector('.insertr-edit-btn');
// Update element content
element.innerHTML = html;
// Re-add edit button
if (editBtn) {
element.appendChild(editBtn);
}
} catch (error) {
console.error('Error parsing markdown:', error);
element.textContent = 'Markdown parsing error: ' + error.message;
}
}
cancelEditing(contentId) {
const element = this.editableElements.get(contentId);
if (!element) return;
@@ -371,6 +468,31 @@ class Insertr {
}
extractContentFromElement(element) {
const config = element._insertrConfig;
if (config.type === 'markdown') {
// For markdown collections, return the stored markdown or extract from cache
const contentId = element.getAttribute('data-content-id');
const cached = this.state.contentCache.get(contentId);
if (cached) {
// Handle both old format (object) and new format (string)
if (typeof cached === 'string') {
return cached;
} else if (cached.text && typeof cached.text === 'string') {
return cached.text;
}
}
// Fallback: extract basic text content (this happens on first edit)
const clone = element.cloneNode(true);
const editBtn = clone.querySelector('.insertr-edit-btn');
if (editBtn) editBtn.remove();
// Convert basic HTML structure to markdown
return this.basicHtmlToMarkdown(clone.innerHTML);
}
// Clone element to avoid modifying original
const clone = element.cloneNode(true);
@@ -384,6 +506,36 @@ class Insertr {
return clone.textContent.trim();
}
basicHtmlToMarkdown(html) {
// Simple conversion for initial content only
let markdown = html;
// Basic paragraph conversion - capture content and trim each paragraph
markdown = markdown.replace(/<p[^>]*>(.*?)<\/p>/gis, (match, content) => {
return content.trim() + '\n\n';
});
markdown = markdown.replace(/<br\s*\/?>/gi, '\n');
// Remove HTML tags
markdown = markdown.replace(/<[^>]*>/g, '');
// Clean up entities
markdown = markdown.replace(/&nbsp;/g, ' ');
markdown = markdown.replace(/&amp;/g, '&');
markdown = markdown.replace(/&lt;/g, '<');
markdown = markdown.replace(/&gt;/g, '>');
// Aggressive whitespace cleanup
markdown = markdown
.split('\n') // Split into lines
.map(line => line.trim()) // Trim each line
.join('\n') // Rejoin
.replace(/\n\n\n+/g, '\n\n') // Multiple blank lines to double
.trim(); // Final trim
return markdown;
}
// Authentication and UI methods (unchanged from previous version)
setupAuthenticationControls() {
const authToggle = document.getElementById('auth-toggle');
@@ -489,6 +641,29 @@ class Insertr {
return div.innerHTML;
}
// Helper method to detect if a paragraph should have 'lead' class
isLeadParagraph(text) {
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')
);
}
// Helper method to detect button-like text
looksLikeButton(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));
}
// Storage methods
loadContentFromStorage() {
try {