var Insertr = (function () { 'use strict'; /** * InsertrCore - Core functionality for content management */ class InsertrCore { constructor(options = {}) { this.options = { apiEndpoint: options.apiEndpoint || '/api/content', siteId: options.siteId || 'default', ...options }; } // Find all enhanced elements on the page findEnhancedElements() { return document.querySelectorAll('[data-insertr-enhanced="true"]'); } // Get element metadata getElementMetadata(element) { return { contentId: element.getAttribute('data-content-id'), contentType: element.getAttribute('data-content-type'), element: element }; } // Get all elements with their metadata getAllElements() { const elements = this.findEnhancedElements(); return Array.from(elements).map(el => this.getElementMetadata(el)); } } /** * InsertrFormRenderer - Professional modal editing forms * Ported from prototype with modern ES6+ architecture */ class InsertrFormRenderer { constructor() { this.currentOverlay = null; this.setupStyles(); } /** * Create and show edit form for content element * @param {Object} meta - Element metadata {element, contentId, contentType} * @param {string} currentContent - Current content value * @param {Function} onSave - Save callback * @param {Function} onCancel - Cancel callback */ showEditForm(meta, currentContent, onSave, onCancel) { // Close any existing form this.closeForm(); const { element, contentId, contentType } = meta; const config = this.getFieldConfig(element, contentType); // Create form const form = this.createEditForm(contentId, config, currentContent); // Create overlay with backdrop const overlay = this.createOverlay(form); // Position form this.positionForm(element, overlay); // Setup event handlers this.setupFormHandlers(form, overlay, { 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; } /** * Close current form */ closeForm() { if (this.currentOverlay) { this.currentOverlay.remove(); this.currentOverlay = null; } } /** * Generate field configuration based on element */ 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...' }, 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...', includeUrl: true }, span: { type: 'text', label: 'Text', placeholder: 'Enter text...' }, button: { type: 'text', label: 'Button Text', placeholder: 'Enter button text...' }, }; let config = configs[tagName] || { type: 'text', label: 'Text', placeholder: 'Enter text...' }; // CSS class enhancements 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 }; } return config; } /** * Create form HTML structure */ createEditForm(contentId, config, currentContent) { const form = document.createElement('div'); form.className = 'insertr-edit-form'; let formHTML = `
${config.label}
`; if (config.type === 'markdown') { formHTML += this.createMarkdownField(config, currentContent); } else if (config.type === 'link' && config.includeUrl) { formHTML += this.createLinkField(config, currentContent); } else if (config.type === 'textarea') { formHTML += this.createTextareaField(config, currentContent); } else { formHTML += this.createTextField(config, currentContent); } // Form buttons formHTML += `
`; form.innerHTML = formHTML; return form; } /** * Create markdown field with preview */ createMarkdownField(config, currentContent) { return `
Supports Markdown formatting (bold, italic, links, etc.)
`; } /** * Create link field (text + URL) */ createLinkField(config, currentContent) { const linkText = typeof currentContent === 'object' ? currentContent.text || '' : currentContent; const linkUrl = typeof currentContent === 'object' ? currentContent.url || '' : ''; return `
`; } /** * Create textarea field */ createTextareaField(config, currentContent) { const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent; return `
`; } /** * Create text input field */ createTextField(config, currentContent) { const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent; return `
`; } /** * Create overlay with backdrop */ createOverlay(form) { const overlay = document.createElement('div'); overlay.className = 'insertr-form-overlay'; overlay.appendChild(form); return overlay; } /** * Position form relative to element */ positionForm(element, overlay) { const rect = element.getBoundingClientRect(); const form = overlay.querySelector('.insertr-edit-form'); // Calculate optimal width (responsive) const viewportWidth = window.innerWidth; let formWidth; if (viewportWidth < 768) { formWidth = Math.min(viewportWidth - 40, 350); } else { formWidth = Math.min(Math.max(rect.width, 300), 500); } form.style.width = `${formWidth}px`; // Position below element with some spacing const top = rect.bottom + window.scrollY + 10; const left = Math.max(20, rect.left + window.scrollX); overlay.style.position = 'absolute'; overlay.style.top = `${top}px`; overlay.style.left = `${left}px`; overlay.style.zIndex = '10000'; } /** * Setup form event handlers */ setupFormHandlers(form, overlay, { onSave, onCancel }) { const saveBtn = form.querySelector('.insertr-btn-save'); const cancelBtn = form.querySelector('.insertr-btn-cancel'); if (saveBtn) { saveBtn.addEventListener('click', () => { const formData = this.extractFormData(form); onSave(formData); }); } if (cancelBtn) { cancelBtn.addEventListener('click', () => { onCancel(); this.closeForm(); }); } // ESC key to cancel const keyHandler = (e) => { if (e.key === 'Escape') { onCancel(); this.closeForm(); document.removeEventListener('keydown', keyHandler); } }; document.addEventListener('keydown', keyHandler); // Click outside to cancel overlay.addEventListener('click', (e) => { if (e.target === overlay) { onCancel(); this.closeForm(); } }); } /** * Extract form data */ extractFormData(form) { const data = {}; // Handle different field types const textInput = form.querySelector('input[name="text"]'); const urlInput = form.querySelector('input[name="url"]'); const contentInput = form.querySelector('input[name="content"], textarea[name="content"]'); if (textInput && urlInput) { // Link field data.text = textInput.value; data.url = urlInput.value; } else if (contentInput) { // Text or textarea field data.text = contentInput.value; } return data; } /** * Escape HTML to prevent XSS */ escapeHtml(text) { if (typeof text !== 'string') return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Setup form styles */ setupStyles() { const styles = ` .insertr-form-overlay { position: absolute; z-index: 10000; } .insertr-edit-form { background: white; border: 2px solid #007cba; border-radius: 8px; padding: 1rem; box-shadow: 0 8px 25px rgba(0,0,0,0.15); width: 100%; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .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; } .insertr-form-group { margin-bottom: 1rem; } .insertr-form-group:last-child { margin-bottom: 0; } .insertr-form-label { display: block; font-weight: 600; color: #374151; margin-bottom: 0.5rem; font-size: 0.875rem; } .insertr-form-input, .insertr-form-textarea { width: 100%; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-family: inherit; font-size: 1rem; transition: border-color 0.2s, box-shadow 0.2s; box-sizing: border-box; } .insertr-form-input:focus, .insertr-form-textarea:focus { outline: none; border-color: #007cba; box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1); } .insertr-form-textarea { min-height: 120px; resize: vertical; 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; } .insertr-form-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #e5e7eb; } .insertr-btn-save { background: #10b981; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; font-size: 0.875rem; } .insertr-btn-save:hover { background: #059669; } .insertr-btn-cancel { background: #6b7280; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; font-size: 0.875rem; } .insertr-btn-cancel:hover { background: #4b5563; } .insertr-form-help { font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem; } `; const styleSheet = document.createElement('style'); styleSheet.type = 'text/css'; styleSheet.innerHTML = styles; document.head.appendChild(styleSheet); } } /** * InsertrEditor - Visual editing functionality */ class InsertrEditor { constructor(core, auth, options = {}) { this.core = core; this.auth = auth; this.options = options; this.isActive = false; this.formRenderer = new InsertrFormRenderer(); } start() { if (this.isActive) return; console.log('🚀 Starting Insertr Editor'); this.isActive = true; // Add editor styles this.addEditorStyles(); // Initialize all enhanced elements const elements = this.core.getAllElements(); console.log(`📝 Found ${elements.length} editable elements`); elements.forEach(meta => this.initializeElement(meta)); } initializeElement(meta) { const { element, contentId, contentType } = meta; // Add visual indicators element.style.cursor = 'pointer'; element.style.position = 'relative'; // Add interaction handlers this.addHoverEffects(element); this.addClickHandler(element, meta); } addHoverEffects(element) { element.addEventListener('mouseenter', () => { element.classList.add('insertr-editing-hover'); }); element.addEventListener('mouseleave', () => { element.classList.remove('insertr-editing-hover'); }); } addClickHandler(element, meta) { element.addEventListener('click', (e) => { // Only allow editing if authenticated and in edit mode if (!this.auth.isAuthenticated() || !this.auth.isEditMode()) { return; // Let normal click behavior happen } e.preventDefault(); this.openEditor(meta); }); } openEditor(meta) { const { element } = meta; const currentContent = this.extractCurrentContent(element); // Show professional form instead of prompt this.formRenderer.showEditForm( meta, currentContent, (formData) => this.handleSave(meta, formData), () => this.handleCancel(meta) ); } extractCurrentContent(element) { // For links, extract both text and URL if (element.tagName.toLowerCase() === 'a') { return { text: element.textContent.trim(), url: element.getAttribute('href') || '' }; } // For other elements, just return text content return element.textContent.trim(); } handleSave(meta, formData) { console.log('💾 Saving content:', meta.contentId, formData); // Update element content based on type this.updateElementContent(meta.element, formData); // Close form this.formRenderer.closeForm(); // TODO: Save to backend API console.log(`✅ Content saved:`, meta.contentId, formData); } handleCancel(meta) { console.log('❌ Edit cancelled:', meta.contentId); } updateElementContent(element, formData) { if (element.tagName.toLowerCase() === 'a') { // Update link element if (formData.text !== undefined) { element.textContent = formData.text; } if (formData.url !== undefined) { element.setAttribute('href', formData.url); } } else { // Update text content element.textContent = formData.text || ''; } } // Legacy method - now handled by handleSave and updateElementContent addEditorStyles() { const styles = ` .insertr-editing-hover { outline: 2px dashed #007cba !important; outline-offset: 2px !important; background-color: rgba(0, 124, 186, 0.05) !important; } [data-insertr-enhanced="true"]:hover::after { content: "✏️ " attr(data-content-type); position: absolute; top: -25px; left: 0; background: #007cba; color: white; padding: 2px 6px; font-size: 11px; border-radius: 3px; white-space: nowrap; z-index: 1000; font-family: monospace; } `; const styleSheet = document.createElement('style'); styleSheet.type = 'text/css'; styleSheet.innerHTML = styles; document.head.appendChild(styleSheet); } } /** * InsertrAuth - Authentication and state management * Handles user authentication, edit mode, and visual state changes */ class InsertrAuth { constructor(options = {}) { this.options = { mockAuth: options.mockAuth !== false, // Enable mock auth by default autoCreateControls: options.autoCreateControls !== false, ...options }; // Authentication state this.state = { isAuthenticated: false, editMode: false, currentUser: null, activeEditor: null }; this.statusIndicator = null; } /** * Initialize authentication system */ init() { console.log('🔐 Initializing Insertr Authentication'); if (this.options.autoCreateControls) { this.createAuthControls(); } this.setupAuthenticationControls(); this.createStatusIndicator(); this.updateBodyClasses(); console.log('📱 Auth controls ready - Look for buttons in top-right corner'); } /** * Create authentication control buttons if they don't exist */ createAuthControls() { // Check if controls already exist if (document.getElementById('insertr-auth-controls')) { return; } const controlsHtml = `
`; // Add controls to page document.body.insertAdjacentHTML('beforeend', controlsHtml); // Add styles for controls this.addControlStyles(); } /** * Setup event listeners for authentication controls */ setupAuthenticationControls() { const authToggle = document.getElementById('insertr-auth-toggle'); const editToggle = document.getElementById('insertr-edit-toggle'); if (authToggle) { authToggle.addEventListener('click', () => this.toggleAuthentication()); } if (editToggle) { editToggle.addEventListener('click', () => this.toggleEditMode()); } } /** * Toggle authentication state */ toggleAuthentication() { this.state.isAuthenticated = !this.state.isAuthenticated; this.state.currentUser = this.state.isAuthenticated ? { name: 'Demo User', email: 'demo@example.com', role: 'editor' } : null; // Reset edit mode when logging out if (!this.state.isAuthenticated) { this.state.editMode = false; } this.updateBodyClasses(); this.updateButtonStates(); this.updateStatusIndicator(); console.log(this.state.isAuthenticated ? '✅ Authenticated as Demo User' : '❌ Logged out'); } /** * Toggle edit mode (only when authenticated) */ toggleEditMode() { if (!this.state.isAuthenticated) { console.warn('❌ Cannot enable edit mode - not authenticated'); return; } this.state.editMode = !this.state.editMode; // Cancel any active editing when turning off edit mode if (!this.state.editMode && this.state.activeEditor) { // This would be handled by the main editor this.state.activeEditor = null; } this.updateBodyClasses(); this.updateButtonStates(); this.updateStatusIndicator(); console.log(this.state.editMode ? '✏️ Edit mode ON - Click elements to edit' : '👀 Edit mode OFF - Read-only view'); } /** * Update body CSS classes based on authentication state */ updateBodyClasses() { document.body.classList.toggle('insertr-authenticated', this.state.isAuthenticated); document.body.classList.toggle('insertr-edit-mode', this.state.editMode); } /** * Update button text and visibility */ updateButtonStates() { const authBtn = document.getElementById('insertr-auth-toggle'); const editBtn = document.getElementById('insertr-edit-toggle'); if (authBtn) { authBtn.textContent = this.state.isAuthenticated ? 'Logout' : 'Login as Client'; authBtn.className = `insertr-auth-btn ${this.state.isAuthenticated ? 'insertr-authenticated' : ''}`; } if (editBtn) { editBtn.style.display = this.state.isAuthenticated ? 'inline-block' : 'none'; editBtn.textContent = `Edit Mode: ${this.state.editMode ? 'On' : 'Off'}`; editBtn.className = `insertr-auth-btn ${this.state.editMode ? 'insertr-edit-active' : ''}`; } } /** * Create status indicator */ createStatusIndicator() { // Check if already exists if (document.getElementById('insertr-status')) { return; } const statusHtml = `
Visitor Mode
`; document.body.insertAdjacentHTML('beforeend', statusHtml); this.statusIndicator = document.getElementById('insertr-status'); this.updateStatusIndicator(); } /** * Update status indicator text and style */ updateStatusIndicator() { const statusText = document.querySelector('.insertr-status-text'); const statusDot = document.querySelector('.insertr-status-dot'); if (!statusText || !statusDot) return; if (!this.state.isAuthenticated) { statusText.textContent = 'Visitor Mode'; statusDot.className = 'insertr-status-dot insertr-status-visitor'; } else if (this.state.editMode) { statusText.textContent = 'Editing'; statusDot.className = 'insertr-status-dot insertr-status-editing'; } else { statusText.textContent = 'Authenticated'; statusDot.className = 'insertr-status-dot insertr-status-authenticated'; } } /** * Check if user is authenticated */ isAuthenticated() { return this.state.isAuthenticated; } /** * Check if edit mode is enabled */ isEditMode() { return this.state.editMode; } /** * Get current user info */ getCurrentUser() { return this.state.currentUser; } /** * Add styles for authentication controls */ addControlStyles() { const styles = ` .insertr-auth-controls { position: fixed; top: 20px; right: 20px; z-index: 9999; display: flex; gap: 10px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .insertr-auth-btn { background: #4f46e5; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .insertr-auth-btn:hover { background: #4338ca; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .insertr-auth-btn.insertr-authenticated { background: #059669; } .insertr-auth-btn.insertr-authenticated:hover { background: #047857; } .insertr-auth-btn.insertr-edit-active { background: #dc2626; } .insertr-auth-btn.insertr-edit-active:hover { background: #b91c1c; } .insertr-status { position: fixed; bottom: 20px; left: 20px; z-index: 9999; background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .insertr-status-content { display: flex; align-items: center; gap: 8px; } .insertr-status-text { font-size: 12px; font-weight: 500; color: #374151; } .insertr-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #9ca3af; } .insertr-status-dot.insertr-status-visitor { background: #9ca3af; } .insertr-status-dot.insertr-status-authenticated { background: #059669; } .insertr-status-dot.insertr-status-editing { background: #dc2626; animation: insertr-pulse 2s infinite; } @keyframes insertr-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } /* Hide editing interface when not in edit mode */ body:not(.insertr-edit-mode) [data-insertr-enhanced]:hover::after { display: none !important; } /* Only show editing features when in edit mode */ .insertr-authenticated.insertr-edit-mode [data-insertr-enhanced] { cursor: pointer; } .insertr-authenticated.insertr-edit-mode [data-insertr-enhanced]:hover { outline: 2px dashed #007cba !important; outline-offset: 2px !important; background-color: rgba(0, 124, 186, 0.05) !important; } `; const styleSheet = document.createElement('style'); styleSheet.type = 'text/css'; styleSheet.innerHTML = styles; document.head.appendChild(styleSheet); } /** * OAuth integration placeholder * In production, this would handle real OAuth flows */ async authenticateWithOAuth(provider = 'google') { // Mock OAuth flow for now console.log(`🔐 Mock OAuth login with ${provider}`); // Simulate OAuth callback setTimeout(() => { this.state.isAuthenticated = true; this.state.currentUser = { name: 'OAuth User', email: 'user@example.com', provider: provider, role: 'editor' }; this.updateBodyClasses(); this.updateButtonStates(); this.updateStatusIndicator(); console.log('✅ OAuth authentication successful'); }, 1000); } } /** * Insertr - The Tailwind of CMS * Main library entry point */ // Create global Insertr instance window.Insertr = { // Core functionality core: null, editor: null, auth: null, // Initialize the library init(options = {}) { console.log('🔧 Insertr v1.0.0 initializing... (Hot Reload Ready)'); this.core = new InsertrCore(options); this.auth = new InsertrAuth(options); this.editor = new InsertrEditor(this.core, this.auth, options); // Auto-initialize if DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.start()); } else { this.start(); } return this; }, // Start the editor and authentication start() { if (this.auth) { this.auth.init(); } if (this.editor) { this.editor.start(); } }, // Public API methods login() { return this.auth ? this.auth.toggleAuthentication() : null; }, logout() { if (this.auth && this.auth.isAuthenticated()) { this.auth.toggleAuthentication(); } }, toggleEditMode() { return this.auth ? this.auth.toggleEditMode() : null; }, isAuthenticated() { return this.auth ? this.auth.isAuthenticated() : false; }, isEditMode() { return this.auth ? this.auth.isEditMode() : false; }, // Version info version: '1.0.0' }; // Auto-initialize in development mode if (document.querySelector('[data-insertr-enhanced]')) { window.Insertr.init(); } var index = window.Insertr; return index; })();