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:
@@ -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>© 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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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(/ /g, ' ');
|
||||
markdown = markdown.replace(/&/g, '&');
|
||||
markdown = markdown.replace(/</g, '<');
|
||||
markdown = markdown.replace(/>/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 {
|
||||
|
||||
Reference in New Issue
Block a user