diff --git a/lib/rollup.config.js b/lib/rollup.config.js index 457143b..e252314 100644 --- a/lib/rollup.config.js +++ b/lib/rollup.config.js @@ -1,5 +1,21 @@ import { nodeResolve } from '@rollup/plugin-node-resolve'; import terser from '@rollup/plugin-terser'; +import { execSync } from 'child_process'; + +// Simple copy plugin to auto-copy to demo-site during development +function copyToDemo() { + return { + name: 'copy-to-demo', + writeBundle() { + try { + execSync('cp dist/insertr.js ../demo-site/insertr.js'); + console.log('📄 Copied to demo-site/insertr.js'); + } catch (error) { + console.warn('⚠️ Failed to copy to demo-site:', error.message); + } + } + }; +} export default [ // Development build @@ -11,7 +27,8 @@ export default [ name: 'Insertr' }, plugins: [ - nodeResolve() + nodeResolve(), + copyToDemo() ] }, // Production build (minified) diff --git a/lib/src/ui/form-renderer.js b/lib/src/ui/form-renderer.js index ddf9845..20d04ae 100644 --- a/lib/src/ui/form-renderer.js +++ b/lib/src/ui/form-renderer.js @@ -1,10 +1,154 @@ /** - * InsertrFormRenderer - Professional modal editing forms - * Ported from prototype with modern ES6+ architecture + * LivePreviewManager - Handles debounced live preview updates + */ +class LivePreviewManager { + constructor() { + this.previewTimeouts = new Map(); + this.activeElement = null; + this.originalContent = null; + this.originalStyles = null; + } + + schedulePreview(element, newValue, elementType) { + const elementId = this.getElementId(element); + + // Clear existing timeout + if (this.previewTimeouts.has(elementId)) { + clearTimeout(this.previewTimeouts.get(elementId)); + } + + // Schedule new preview update with 500ms debounce + const timeoutId = setTimeout(() => { + this.updatePreview(element, newValue, elementType); + }, 500); + + this.previewTimeouts.set(elementId, timeoutId); + } + + updatePreview(element, newValue, elementType) { + // Store original content if first preview + if (!this.originalContent && this.activeElement === element) { + this.originalContent = this.extractOriginalContent(element, elementType); + } + + // Apply preview styling and content + this.applyPreviewContent(element, newValue, elementType); + } + + extractOriginalContent(element, elementType) { + switch (elementType) { + case 'link': + return { + text: element.textContent, + url: element.href + }; + default: + return element.textContent; + } + } + + applyPreviewContent(element, newValue, elementType) { + // Add preview indicator + element.classList.add('insertr-preview-active'); + + // Update content based on element type + switch (elementType) { + case 'text': + case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': + case 'span': case 'button': + if (newValue && newValue.trim()) { + element.textContent = newValue; + } + break; + + case 'textarea': + case 'p': + if (newValue && newValue.trim()) { + element.textContent = newValue; + } + break; + + case 'link': + if (typeof newValue === 'object') { + if (newValue.text !== undefined && newValue.text.trim()) { + element.textContent = newValue.text; + } + if (newValue.url !== undefined && newValue.url.trim()) { + element.href = newValue.url; + } + } else if (newValue && newValue.trim()) { + element.textContent = newValue; + } + break; + + case 'markdown': + // For markdown, show raw text preview + if (newValue && newValue.trim()) { + element.textContent = newValue; + } + break; + } + } + + clearPreview(element) { + if (!element) return; + + const elementId = this.getElementId(element); + + // Clear any pending preview + if (this.previewTimeouts.has(elementId)) { + clearTimeout(this.previewTimeouts.get(elementId)); + this.previewTimeouts.delete(elementId); + } + + // Restore original content + if (this.originalContent && element === this.activeElement) { + this.restoreOriginalContent(element); + } + + // Remove preview styling + element.classList.remove('insertr-preview-active'); + this.activeElement = null; + this.originalContent = null; + } + + restoreOriginalContent(element) { + if (!this.originalContent) return; + + if (typeof this.originalContent === 'object') { + // Link element + element.textContent = this.originalContent.text; + if (this.originalContent.url) { + element.href = this.originalContent.url; + } + } else { + // Text element + element.textContent = this.originalContent; + } + } + + getElementId(element) { + // Create unique ID for element tracking + if (!element._insertrId) { + element._insertrId = 'insertr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + } + return element._insertrId; + } + + setActiveElement(element) { + this.activeElement = element; + this.originalContent = null; + } +} + +/** + * InsertrFormRenderer - Professional modal editing forms with live preview + * Enhanced with debounced live preview and comfortable input sizing */ export class InsertrFormRenderer { constructor() { this.currentOverlay = null; + this.previewManager = new LivePreviewManager(); this.setupStyles(); } @@ -21,29 +165,32 @@ export class InsertrFormRenderer { const { element, contentId, contentType } = meta; const config = this.getFieldConfig(element, contentType); - + + // Initialize preview manager for this element + this.previewManager.setActiveElement(element); + // Create form const form = this.createEditForm(contentId, config, currentContent); - + // Create overlay with backdrop const overlay = this.createOverlay(form); - - // Position form + + // Position form with enhanced sizing this.positionForm(element, overlay); - - // Setup event handlers - this.setupFormHandlers(form, overlay, { onSave, onCancel }); - + + // Setup event handlers with live preview + this.setupFormHandlers(form, overlay, element, config, { onSave, onCancel }); + // Show form document.body.appendChild(overlay); this.currentOverlay = overlay; - + // Focus first input const firstInput = form.querySelector('input, textarea'); if (firstInput) { setTimeout(() => firstInput.focus(), 100); } - + return overlay; } @@ -51,6 +198,11 @@ export class InsertrFormRenderer { * Close current form */ closeForm() { + // Clear any active previews + if (this.previewManager.activeElement) { + this.previewManager.clearPreview(this.previewManager.activeElement); + } + if (this.currentOverlay) { this.currentOverlay.remove(); this.currentOverlay = null; @@ -63,7 +215,7 @@ export class InsertrFormRenderer { getFieldConfig(element, contentType) { const tagName = element.tagName.toLowerCase(); const classList = Array.from(element.classList); - + // Default configurations based on element type const configs = { h1: { type: 'text', label: 'Headline', maxLength: 60, placeholder: 'Enter headline...' }, @@ -84,7 +236,7 @@ export class InsertrFormRenderer { if (classList.includes('lead')) { config = { ...config, label: 'Lead Paragraph', rows: 4, placeholder: 'Enter lead paragraph...' }; } - + // Override with contentType from CLI if specified if (contentType === 'markdown') { config = { ...config, type: 'markdown', label: 'Markdown Content', rows: 8 }; @@ -99,9 +251,9 @@ export class InsertrFormRenderer { createEditForm(contentId, config, currentContent) { const form = document.createElement('div'); form.className = 'insertr-edit-form'; - + let formHTML = `