refactor: Extract UI into dedicated control panel module
- Create InsertrControlPanel class for unified UI management - Separate business logic from presentation layer - Remove DOM manipulation from auth.js and editor.js - Add comprehensive CSS for status indicators and editing effects - Implement consistent kebab-case file naming - Add event-driven communication between core and UI layers UI Architecture: - Unified control panel with status indicator and action buttons - Color-coded status dots (gray/blue/green for visitor/auth/editing) - Professional editing hover effects with tooltips - Responsive design for mobile devices - Proper z-index and accessibility management Business Logic: - Pure auth.js focused on authentication state and OAuth flows - Pure editor.js focused on content editing workflow - Event emitters for state changes between modules - Clean separation of concerns and testable architecture
This commit is contained in:
371
lib/src/ui/control-panel.js
Normal file
371
lib/src/ui/control-panel.js
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* InsertrControlPanel - Unified UI Controller
|
||||
* Handles all presentation layer concerns for the insertr system
|
||||
*
|
||||
* Architecture:
|
||||
* - Pure presentation layer - no business logic
|
||||
* - Event-driven communication with core modules
|
||||
* - Unified control panel design
|
||||
* - Responsive and accessible
|
||||
*/
|
||||
export class InsertrControlPanel {
|
||||
constructor(auth, editor, apiClient, options = {}) {
|
||||
this.auth = auth;
|
||||
this.editor = editor;
|
||||
this.apiClient = apiClient;
|
||||
this.options = options;
|
||||
|
||||
// UI state
|
||||
this.elements = {};
|
||||
this.isInitialized = false;
|
||||
|
||||
// Bind methods for event listeners
|
||||
this.handleAuthToggle = this.handleAuthToggle.bind(this);
|
||||
this.handleEditToggle = this.handleEditToggle.bind(this);
|
||||
this.handleEnhanceClick = this.handleEnhanceClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the control panel (called after DOM is ready)
|
||||
*/
|
||||
init() {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
console.log('🎨 Initializing Insertr Control Panel');
|
||||
this.createControlPanel();
|
||||
this.setupEventListeners();
|
||||
this.updateVisualState();
|
||||
this.isInitialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the unified control panel structure
|
||||
*/
|
||||
createControlPanel() {
|
||||
// Check if control panel already exists
|
||||
if (document.getElementById('insertr-control-panel')) {
|
||||
this.cacheElements();
|
||||
return;
|
||||
}
|
||||
|
||||
const controlPanelHtml = `
|
||||
<div id="insertr-control-panel" class="insertr-control-panel">
|
||||
<!-- Status Indicator Section -->
|
||||
<div id="insertr-status-section" class="insertr-status-section">
|
||||
<div id="insertr-status-indicator" class="insertr-status-indicator">
|
||||
<span class="insertr-status-text">Visitor Mode</span>
|
||||
<span class="insertr-status-dot insertr-status-visitor"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons Section -->
|
||||
<div id="insertr-action-section" class="insertr-action-section">
|
||||
<button id="insertr-enhance-btn" class="insertr-action-btn insertr-enhance-btn"
|
||||
style="display: none;"
|
||||
title="Enhance files with latest content">
|
||||
🔄 Enhance
|
||||
</button>
|
||||
<button id="insertr-edit-toggle" class="insertr-action-btn insertr-edit-btn"
|
||||
style="display: none;">
|
||||
Edit Mode: Off
|
||||
</button>
|
||||
<button id="insertr-auth-toggle" class="insertr-action-btn insertr-auth-btn">
|
||||
Login as Client
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add control panel to page
|
||||
document.body.insertAdjacentHTML('beforeend', controlPanelHtml);
|
||||
this.cacheElements();
|
||||
|
||||
console.log('📱 Control panel created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache DOM elements for performance
|
||||
*/
|
||||
cacheElements() {
|
||||
this.elements = {
|
||||
controlPanel: document.getElementById('insertr-control-panel'),
|
||||
statusSection: document.getElementById('insertr-status-section'),
|
||||
statusIndicator: document.getElementById('insertr-status-indicator'),
|
||||
statusText: document.querySelector('.insertr-status-text'),
|
||||
statusDot: document.querySelector('.insertr-status-dot'),
|
||||
actionSection: document.getElementById('insertr-action-section'),
|
||||
authToggle: document.getElementById('insertr-auth-toggle'),
|
||||
editToggle: document.getElementById('insertr-edit-toggle'),
|
||||
enhanceBtn: document.getElementById('insertr-enhance-btn')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for all UI interactions
|
||||
*/
|
||||
setupEventListeners() {
|
||||
if (this.elements.authToggle) {
|
||||
this.elements.authToggle.addEventListener('click', this.handleAuthToggle);
|
||||
}
|
||||
|
||||
if (this.elements.editToggle) {
|
||||
this.elements.editToggle.addEventListener('click', this.handleEditToggle);
|
||||
}
|
||||
|
||||
if (this.elements.enhanceBtn) {
|
||||
this.elements.enhanceBtn.addEventListener('click', this.handleEnhanceClick);
|
||||
}
|
||||
|
||||
// Listen for auth state changes
|
||||
if (this.auth && typeof this.auth.on === 'function') {
|
||||
this.auth.on('stateChange', () => this.updateVisualState());
|
||||
}
|
||||
|
||||
// Listen for editor state changes
|
||||
if (this.editor && typeof this.editor.on === 'function') {
|
||||
this.editor.on('modeChange', () => this.updateVisualState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle authentication toggle click
|
||||
*/
|
||||
handleAuthToggle() {
|
||||
if (this.auth && typeof this.auth.toggleAuthentication === 'function') {
|
||||
this.auth.toggleAuthentication();
|
||||
this.updateVisualState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle edit mode toggle click
|
||||
*/
|
||||
handleEditToggle() {
|
||||
if (this.auth && typeof this.auth.toggleEditMode === 'function') {
|
||||
this.auth.toggleEditMode();
|
||||
this.updateVisualState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle enhance button click
|
||||
*/
|
||||
async handleEnhanceClick() {
|
||||
if (!this.elements.enhanceBtn) return;
|
||||
|
||||
const enhanceBtn = this.elements.enhanceBtn;
|
||||
const siteId = window.insertrConfig?.siteId || this.options.siteId || 'demo';
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
enhanceBtn.textContent = '⏳ Enhancing...';
|
||||
enhanceBtn.disabled = true;
|
||||
|
||||
// Smart server detection for enhance API
|
||||
const isDevelopment = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1';
|
||||
const enhanceUrl = isDevelopment
|
||||
? `http://localhost:8080/api/enhance?site_id=${siteId}`
|
||||
: `/api/enhance?site_id=${siteId}`;
|
||||
|
||||
// Call enhance API
|
||||
const response = await fetch(enhanceUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.auth?.getCurrentUser()?.token || 'mock-token'}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Enhancement failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('✅ Files enhanced successfully:', result);
|
||||
|
||||
// Show success state briefly
|
||||
enhanceBtn.textContent = '✅ Enhanced!';
|
||||
|
||||
// Reset button after success
|
||||
setTimeout(() => {
|
||||
enhanceBtn.textContent = '🔄 Enhance';
|
||||
enhanceBtn.disabled = false;
|
||||
}, 2000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Enhancement failed:', error);
|
||||
enhanceBtn.textContent = '❌ Failed';
|
||||
|
||||
// Reset button after error
|
||||
setTimeout(() => {
|
||||
enhanceBtn.textContent = '🔄 Enhance';
|
||||
enhanceBtn.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all visual elements based on current state
|
||||
*/
|
||||
updateVisualState() {
|
||||
if (!this.isInitialized || !this.elements.statusText) return;
|
||||
|
||||
const isAuthenticated = this.auth ? this.auth.isAuthenticated() : false;
|
||||
const isEditMode = this.auth ? this.auth.isEditMode() : false;
|
||||
|
||||
// Update status indicator
|
||||
this.updateStatusIndicator(isAuthenticated, isEditMode);
|
||||
|
||||
// Update button states
|
||||
this.updateButtonStates(isAuthenticated, isEditMode);
|
||||
|
||||
// Update body classes for global styling
|
||||
this.updateBodyClasses(isAuthenticated, isEditMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status indicator text and visual state
|
||||
*/
|
||||
updateStatusIndicator(isAuthenticated, isEditMode) {
|
||||
const { statusText, statusDot } = this.elements;
|
||||
|
||||
if (!statusText || !statusDot) return;
|
||||
|
||||
if (!isAuthenticated) {
|
||||
statusText.textContent = 'Visitor Mode';
|
||||
statusDot.className = 'insertr-status-dot insertr-status-visitor';
|
||||
} else if (isEditMode) {
|
||||
statusText.textContent = 'Editing';
|
||||
statusDot.className = 'insertr-status-dot insertr-status-editing';
|
||||
} else {
|
||||
statusText.textContent = 'Authenticated';
|
||||
statusDot.className = 'insertr-status-dot insertr-status-authenticated';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update button text, visibility, and states
|
||||
*/
|
||||
updateButtonStates(isAuthenticated, isEditMode) {
|
||||
const { authToggle, editToggle, enhanceBtn } = this.elements;
|
||||
|
||||
// Update auth toggle button
|
||||
if (authToggle) {
|
||||
authToggle.textContent = isAuthenticated ? 'Logout' : 'Login as Client';
|
||||
authToggle.className = `insertr-action-btn insertr-auth-btn ${
|
||||
isAuthenticated ? 'insertr-authenticated' : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
// Update edit mode toggle button
|
||||
if (editToggle) {
|
||||
editToggle.style.display = isAuthenticated ? 'inline-block' : 'none';
|
||||
editToggle.textContent = `Edit Mode: ${isEditMode ? 'On' : 'Off'}`;
|
||||
editToggle.className = `insertr-action-btn insertr-edit-btn ${
|
||||
isEditMode ? 'insertr-edit-active' : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
// Update enhance button (dev-mode only)
|
||||
if (enhanceBtn) {
|
||||
enhanceBtn.style.display = isAuthenticated ? 'inline-block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update body CSS classes for global styling hooks
|
||||
*/
|
||||
updateBodyClasses(isAuthenticated, isEditMode) {
|
||||
document.body.classList.toggle('insertr-authenticated', isAuthenticated);
|
||||
document.body.classList.toggle('insertr-edit-mode', isEditMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add editing indicators to content elements
|
||||
*/
|
||||
addEditingIndicators() {
|
||||
if (!this.editor || !this.editor.core) return;
|
||||
|
||||
const elements = this.editor.core.getAllElements();
|
||||
console.log(`🎨 Adding editing indicators to ${elements.length} elements`);
|
||||
|
||||
elements.forEach(meta => {
|
||||
this.initializeElementVisuals(meta.element);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize visual editing indicators for a single element
|
||||
*/
|
||||
initializeElementVisuals(element) {
|
||||
// Add visual indicators
|
||||
element.style.cursor = 'pointer';
|
||||
element.style.position = 'relative';
|
||||
|
||||
// Add hover effects
|
||||
element.addEventListener('mouseenter', () => {
|
||||
if (this.auth && this.auth.isAuthenticated() && this.auth.isEditMode()) {
|
||||
element.classList.add('insertr-editing-hover');
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener('mouseleave', () => {
|
||||
element.classList.remove('insertr-editing-hover');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove editing indicators (when edit mode is disabled)
|
||||
*/
|
||||
removeEditingIndicators() {
|
||||
const elements = document.querySelectorAll('.insertr-editing-hover');
|
||||
elements.forEach(element => {
|
||||
element.classList.remove('insertr-editing-hover');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show status message to user
|
||||
*/
|
||||
showStatusMessage(message, type = 'info', duration = 3000) {
|
||||
// Remove existing status message
|
||||
const existing = document.getElementById('insertr-status-message');
|
||||
if (existing) {
|
||||
existing.remove();
|
||||
}
|
||||
|
||||
const messageHtml = `
|
||||
<div id="insertr-status-message" class="insertr-status-message ${type}">
|
||||
<p class="insertr-status-text">${message}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', messageHtml);
|
||||
|
||||
// Auto-remove after duration
|
||||
setTimeout(() => {
|
||||
const messageEl = document.getElementById('insertr-status-message');
|
||||
if (messageEl) {
|
||||
messageEl.remove();
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the control panel (cleanup)
|
||||
*/
|
||||
destroy() {
|
||||
if (this.elements.controlPanel) {
|
||||
this.elements.controlPanel.remove();
|
||||
}
|
||||
|
||||
this.elements = {};
|
||||
this.isInitialized = false;
|
||||
|
||||
// Remove body classes
|
||||
document.body.classList.remove('insertr-authenticated', 'insertr-edit-mode');
|
||||
|
||||
console.log('🗑️ Control panel destroyed');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user