/** * Insertr - Element-Level Edit-in-place CMS Library * Modular architecture with configuration system */ class Insertr { constructor(options = {}) { this.options = { apiEndpoint: options.apiEndpoint || '/api/content', authEndpoint: options.authEndpoint || '/api/auth', autoInit: options.autoInit !== false, ...options }; // Core state this.state = { isAuthenticated: false, editMode: false, currentUser: null, activeEditor: null }; this.editableElements = new Map(); this.statusIndicator = null; // Initialize modules this.config = new InsertrConfig(options.config); this.validation = new InsertrValidation(this.config); this.formRenderer = new InsertrFormRenderer(this.validation); this.contentManager = new InsertrContentManager(options); this.markdownProcessor = new InsertrMarkdownProcessor(); if (this.options.autoInit) { this.init(); } } /** * Initialize the CMS system */ async init() { console.log('🚀 Insertr initializing with modular architecture...'); // Scan for editable elements this.scanForEditableElements(); // Setup authentication controls this.setupAuthenticationControls(); // Create status indicator this.createStatusIndicator(); // Apply initial state this.updateBodyClasses(); console.log(`📝 Found ${this.editableElements.size} editable elements`); } /** * Scan for editable elements and set them up */ scanForEditableElements() { const elements = document.querySelectorAll('.insertr'); elements.forEach(element => { const contentId = element.getAttribute('data-content-id'); if (!contentId) { console.warn('Insertr element missing data-content-id:', element); return; } this.editableElements.set(contentId, element); this.setupEditableElement(element, contentId); }); } /** * Setup individual editable element * @param {HTMLElement} element - Element to setup * @param {string} contentId - Content identifier */ setupEditableElement(element, contentId) { // Generate field configuration const fieldConfig = this.config.generateFieldConfig(element); element._insertrConfig = fieldConfig; // Add edit button this.addEditButton(element, contentId); // Load saved content if available const savedContent = this.contentManager.getContent(contentId); if (savedContent) { this.contentManager.applyContentToElement(element, savedContent); } } /** * Add edit button to element * @param {HTMLElement} element - Element to add button to * @param {string} contentId - Content identifier */ addEditButton(element, contentId) { // 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); } /** * Start editing an element * @param {string} contentId - Content identifier */ 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.contentManager.extractContentFromElement(element); // Create and show edit form const form = this.formRenderer.createEditForm(contentId, config, currentContent); const overlay = this.formRenderer.showEditForm(element, form); // Setup form event handlers this.setupFormHandlers(overlay, contentId); this.state.activeEditor = contentId; } /** * Setup form event handlers * @param {HTMLElement} overlay - Form overlay * @param {string} contentId - Content identifier */ setupFormHandlers(overlay, contentId) { const saveBtn = overlay.querySelector('.insertr-btn-save'); const cancelBtn = overlay.querySelector('.insertr-btn-cancel'); if (saveBtn) { saveBtn.addEventListener('click', () => { this.saveElementContent(contentId, overlay); }); } if (cancelBtn) { cancelBtn.addEventListener('click', () => { this.cancelEditing(contentId); }); } // Handle Enter to save, Escape to cancel overlay.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); this.saveElementContent(contentId, overlay); } else if (e.key === 'Escape') { e.preventDefault(); this.cancelEditing(contentId); } }); } /** * Save element content * @param {string} contentId - Content identifier * @param {HTMLElement} overlay - Form overlay */ async saveElementContent(contentId, overlay) { const element = this.editableElements.get(contentId); const form = overlay.querySelector('.insertr-edit-form'); const config = element._insertrConfig; if (!element || !form) return; // Extract form data const formData = this.formRenderer.extractFormData(form, config); // Validate the data const validation = this.validateFormData(formData, config); if (!validation.valid) { alert(validation.message); return; } try { // Show saving state element.classList.add('insertr-saving'); // Save to server (mock for now) await this.contentManager.saveToServer(contentId, formData); // Apply content to element this.contentManager.applyContentToElement(element, formData); // Close form this.formRenderer.hideEditForm(overlay); this.state.activeEditor = null; // Show success feedback element.classList.add('insertr-save-success'); setTimeout(() => { element.classList.remove('insertr-save-success'); }, 2000); } catch (error) { console.error('Failed to save content:', error); alert('Failed to save content. Please try again.'); } finally { element.classList.remove('insertr-saving'); } } /** * Validate form data before saving * @param {string|Object} data - Form data to validate * @param {Object} config - Field configuration * @returns {Object} Validation result */ validateFormData(data, config) { if (config.type === 'link' && config.includeUrl) { // Validate link data const textValidation = this.validation.validateInput(data.text, 'text'); if (!textValidation.valid) return textValidation; const urlValidation = this.validation.validateInput(data.url, 'link'); if (!urlValidation.valid) return urlValidation; return { valid: true }; } else { // Validate single content return this.validation.validateInput(data, config.type); } } /** * Cancel editing * @param {string} contentId - Content identifier */ cancelEditing(contentId) { const overlay = document.querySelector('.insertr-form-overlay'); if (overlay) { this.formRenderer.hideEditForm(overlay); } if (this.state.activeEditor === contentId) { this.state.activeEditor = null; } } /** * Update markdown preview (called by form renderer) * @param {HTMLElement} previewElement - Preview container * @param {string} markdown - Markdown content */ updateMarkdownPreview(previewElement, markdown) { if (this.markdownProcessor.isReady()) { const html = this.markdownProcessor.createPreview(markdown); previewElement.innerHTML = html; } else { previewElement.innerHTML = '

Markdown processor not available

'; } } /** * Render markdown content (called by content manager) * @param {HTMLElement} element - Element to update * @param {string} markdownText - Markdown content */ renderMarkdown(element, markdownText) { if (this.markdownProcessor.isReady()) { this.markdownProcessor.applyToElement(element, markdownText); } else { console.warn('Markdown processor not available'); element.textContent = markdownText; } } // Authentication and UI methods (simplified) /** * Setup authentication controls */ setupAuthenticationControls() { const authToggle = document.getElementById('auth-toggle'); const editToggle = document.getElementById('edit-mode-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' } : null; if (!this.state.isAuthenticated) { this.state.editMode = false; } this.updateBodyClasses(); this.updateStatusIndicator(); const authBtn = document.getElementById('auth-toggle'); if (authBtn) { authBtn.textContent = this.state.isAuthenticated ? 'Logout' : 'Login as Client'; } } /** * Toggle edit mode */ toggleEditMode() { if (!this.state.isAuthenticated) return; this.state.editMode = !this.state.editMode; if (!this.state.editMode && this.state.activeEditor) { this.cancelEditing(this.state.activeEditor); } this.updateBodyClasses(); this.updateStatusIndicator(); const editBtn = document.getElementById('edit-mode-toggle'); if (editBtn) { editBtn.textContent = `Edit Mode: ${this.state.editMode ? 'On' : 'Off'}`; } } /** * Update body CSS classes based on state */ updateBodyClasses() { document.body.classList.toggle('insertr-authenticated', this.state.isAuthenticated); document.body.classList.toggle('insertr-edit-mode', this.state.editMode); const editToggle = document.getElementById('edit-mode-toggle'); if (editToggle) { editToggle.style.display = this.state.isAuthenticated ? 'inline-block' : 'none'; } } /** * Create status indicator */ createStatusIndicator() { // Implementation similar to original, simplified for brevity this.updateStatusIndicator(); } /** * Update status indicator */ updateStatusIndicator() { // Implementation similar to original, simplified for brevity console.log(`Status: Auth=${this.state.isAuthenticated}, Edit=${this.state.editMode}`); } /** * Get configuration instance (for external customization) * @returns {InsertrConfig} Configuration instance */ getConfig() { return this.config; } /** * Get content manager instance * @returns {InsertrContentManager} Content manager instance */ getContentManager() { return this.contentManager; } } // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = Insertr; } // Auto-initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.insertr = new Insertr(); });