- Remove all manual data-content-id attributes from HTML files - Archive old insertr JS/CSS assets to demo-site/archive/ - Remove hardcoded script includes and CSS links - Remove old authentication controls and mock API - Enable pure zero-config approach with class='insertr' only - Parser now generates all 40 content IDs automatically
409 lines
13 KiB
JavaScript
409 lines
13 KiB
JavaScript
/**
|
|
* 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 = '<p><em>Markdown processor not available</em></p>';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}); |