Files
roi-theme/_planificacion/01-design-system/11-JAVASCRIPT-PATTERNS.md
FrankZamora 0f6387ab46 refactor: reorganizar openspec y planificacion con spec recaptcha
- renombrar openspec/ a _openspec/ (carpeta auxiliar)
- mover specs de features a changes/
- crear specs base: arquitectura-limpia, estandares-codigo, nomenclatura
- migrar _planificacion/ con design-system y roi-theme-template
- agregar especificacion recaptcha anti-spam (proposal, tasks, spec)
- corregir rutas y referencias en todas las specs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 15:30:45 -06:00

15 KiB
Raw Blame History

💻 JAVASCRIPT PATTERNS

1. Inicialización del Componente

document.addEventListener('DOMContentLoaded', function() {
    console.log('✅ [ComponentName] Admin Panel cargado');

    // 1. Cargar configuración guardada
    loadConfig();

    // 2. Inicializar vista previa
    updatePreview();

    // 3. Conectar event listeners
    initializeEventListeners();
});

Orden de ejecución:

  1. loadConfig(): Carga valores guardados desde localStorage/JSON
  2. updatePreview(): Renderiza el preview inicial
  3. initializeEventListeners(): Conecta los campos al preview

2. Event Listeners

Patrón Básico

function initializeEventListeners() {
    // Lista de campos que deben actualizar el preview
    const fields = [
        'enabled',
        'showOnMobile',
        'bgColor',
        'textColor',
        'highlightText',
        'messageText',
        'showLink',
        'linkText',
        'linkUrl'
    ];

    // Conectar event listeners automáticamente
    fields.forEach(fieldId => {
        const element = document.getElementById(fieldId);
        if (element) {
            // Checkboxes: usar 'change'
            if (element.type === 'checkbox') {
                element.addEventListener('change', updatePreview);
            }
            // Otros inputs: usar 'input' para tiempo real
            else {
                element.addEventListener('input', updatePreview);
            }
        }
    });

    // Event listeners específicos
    initializeColorPickerListeners();
    initializeTextareaCounters();
    initializeResetButton();
}

Color Pickers con Display Hex

function initializeColorPickerListeners() {
    const colorFields = [
        { input: 'bgColor', display: 'bgColorValue' },
        { input: 'textColor', display: 'textColorValue' },
        { input: 'highlightColor', display: 'highlightColorValue' }
    ];

    colorFields.forEach(({ input, display }) => {
        const inputElement = document.getElementById(input);
        const displayElement = document.getElementById(display);

        if (inputElement && displayElement) {
            inputElement.addEventListener('input', function() {
                displayElement.textContent = this.value.toUpperCase();
                updatePreview();
            });
        }
    });
}

Textareas con Contadores

function initializeTextareaCounters() {
    const textareas = [
        { field: 'messageText', counter: 'messageTextCount', progress: 'messageTextProgress', max: 250 },
        { field: 'description', counter: 'descriptionCount', progress: 'descriptionProgress', max: 500 }
    ];

    textareas.forEach(({ field, counter, progress, max }) => {
        const textarea = document.getElementById(field);
        const counterElement = document.getElementById(counter);
        const progressElement = document.getElementById(progress);

        if (textarea && counterElement && progressElement) {
            textarea.addEventListener('input', function() {
                const length = this.value.length;
                const percentage = (length / max) * 100;

                // Actualizar contador
                counterElement.textContent = length;

                // Actualizar progress bar
                progressElement.style.width = percentage + '%';
                progressElement.setAttribute('aria-valuenow', length);

                // Cambiar color según uso
                if (percentage > 90) {
                    progressElement.style.backgroundColor = '#dc3545'; // Rojo
                } else if (percentage > 75) {
                    progressElement.style.backgroundColor = '#ffc107'; // Amarillo
                } else {
                    progressElement.style.backgroundColor = '#FF8600'; // Orange
                }

                updatePreview();
            });
        }
    });
}

Botón de Reset

function initializeResetButton() {
    const resetBtn = document.getElementById('resetDefaults');
    if (resetBtn) {
        resetBtn.addEventListener('click', resetToDefaults);
    }
}

3. Función updatePreview()

/**
 * Actualiza la vista previa en tiempo real
 * REGLA: Solo modificar propiedades que el usuario puede cambiar
 */
function updatePreview() {
    const preview = document.getElementById('componentPreview');
    if (!preview) return;

    // 1. Activar/desactivar componente
    const enabled = document.getElementById('enabled').checked;
    preview.style.display = enabled ? 'block' : 'none';

    // 2. Colores
    const bgColor = document.getElementById('bgColor').value;
    const textColor = document.getElementById('textColor').value;
    preview.style.backgroundColor = bgColor;
    preview.style.color = textColor;

    // 3. Textos
    const highlightText = document.getElementById('highlightText').value;
    const messageText = document.getElementById('messageText').value;

    const highlightElement = document.getElementById('previewHighlight');
    const messageElement = document.getElementById('previewMessage');

    if (highlightElement) highlightElement.textContent = highlightText;
    if (messageElement) messageElement.textContent = messageText;

    // 4. Mostrar/ocultar elementos
    const showIcon = document.getElementById('showIcon').checked;
    const iconElement = document.getElementById('previewIcon');
    if (iconElement) {
        iconElement.style.display = showIcon ? 'inline-block' : 'none';
    }

    // 5. Cambiar clase del icono
    const iconClass = document.getElementById('iconClass').value;
    if (iconElement && iconClass) {
        iconElement.className = iconClass + ' notification-icon';
    }

    // 6. Link
    const showLink = document.getElementById('showLink').checked;
    const linkElement = document.getElementById('previewLink');

    if (linkElement) {
        linkElement.style.display = showLink ? 'inline-block' : 'none';

        if (showLink) {
            const linkText = document.getElementById('linkText').value;
            const linkUrl = document.getElementById('linkUrl').value;
            const linkTarget = document.getElementById('linkTarget').value;

            linkElement.textContent = linkText;
            linkElement.href = linkUrl;
            linkElement.target = linkTarget;
        }
    }
}

4. Guardar Configuración

localStorage (Temporal)

/**
 * Guarda la configuración en localStorage
 */
function saveConfig() {
    const config = {
        enabled: document.getElementById('enabled').checked,
        showOnMobile: document.getElementById('showOnMobile').checked,
        showOnDesktop: document.getElementById('showOnDesktop').checked,
        bgColor: document.getElementById('bgColor').value,
        textColor: document.getElementById('textColor').value,
        highlightColor: document.getElementById('highlightColor').value,
        highlightText: document.getElementById('highlightText').value,
        messageText: document.getElementById('messageText').value,
        showIcon: document.getElementById('showIcon').checked,
        iconClass: document.getElementById('iconClass').value,
        showLink: document.getElementById('showLink').checked,
        linkText: document.getElementById('linkText').value,
        linkUrl: document.getElementById('linkUrl').value,
        linkTarget: document.getElementById('linkTarget').value
    };

    localStorage.setItem('componentConfig', JSON.stringify(config));
    console.log('💾 Configuración guardada:', config);
}

Archivo JSON (Persistente)

/**
 * Guarda la configuración en archivo JSON
 */
async function saveConfigToFile() {
    const config = {
        component: '[component-name]',  // Nombre del componente en kebab-case
        version: '1.0',
        lastModified: new Date().toISOString(),
        config: {
            enabled: document.getElementById('enabled').checked,
            bgColor: document.getElementById('bgColor').value,
            textColor: document.getElementById('textColor').value,
            messageText: document.getElementById('messageText').value,
            // ... todos los campos del componente
        }
    };

    try {
        const response = await fetch('./config.json', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify(config, null, 2)
        });

        if (response.ok) {
            console.log('💾 Configuración guardada exitosamente');
            showNotification('Cambios guardados', 'success');
        } else {
            throw new Error('Error al guardar');
        }
    } catch (error) {
        console.error('❌ Error al guardar:', error);
        showNotification('Error al guardar cambios', 'error');
    }
}

5. Cargar Configuración

Desde localStorage

/**
 * Carga la configuración desde localStorage
 */
function loadConfig() {
    const saved = localStorage.getItem('componentConfig');

    if (!saved) {
        console.log(' No hay configuración guardada, usando valores por defecto');
        return;
    }

    const config = JSON.parse(saved);

    // Aplicar valores guardados
    Object.keys(config).forEach(key => {
        const element = document.getElementById(key);
        if (element) {
            if (element.type === 'checkbox') {
                element.checked = config[key];
            } else if (element.type === 'color') {
                element.value = config[key];
                // Actualizar display del hex
                const displayElement = document.getElementById(key + 'Value');
                if (displayElement) {
                    displayElement.textContent = config[key].toUpperCase();
                }
            } else {
                element.value = config[key];
            }
        }
    });

    console.log('📂 Configuración cargada:', config);
    updatePreview();
}

Desde archivo JSON

/**
 * Carga la configuración desde archivo JSON
 */
async function loadConfigFromFile() {
    try {
        const response = await fetch('./config.json');

        if (!response.ok) {
            throw new Error('Config file not found');
        }

        const data = await response.json();
        const config = data.config;

        // Aplicar valores cargados (igual que con localStorage)
        Object.keys(config).forEach(key => {
            const element = document.getElementById(key);
            if (element) {
                if (element.type === 'checkbox') {
                    element.checked = config[key];
                } else {
                    element.value = config[key];
                }
            }
        });

        console.log('📂 Configuración cargada desde archivo:', config);
        updatePreview();

    } catch (error) {
        console.log(' No se encontró config.json, usando valores por defecto');
        resetToDefaults();
    }
}

6. Reset a Valores por Defecto

/**
 * Restaura los valores por defecto
 */
function resetToDefaults() {
    if (!confirm('¿Estás seguro de restaurar los valores por defecto?')) {
        return;
    }

    // Valores por defecto
    const defaults = {
        enabled: true,
        showOnMobile: true,
        showOnDesktop: true,
        bgColor: '#0E2337',
        textColor: '#ffffff',
        highlightColor: '#FF8600',
        highlightText: 'Nuevo:',
        messageText: 'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.',
        showIcon: true,
        iconClass: 'bi bi-megaphone-fill',
        showLink: true,
        linkText: 'Ver Catálogo →',
        linkUrl: '/catalogo',
        linkTarget: '_self',
        fontSize: 'normal'
    };

    // Aplicar defaults
    Object.keys(defaults).forEach(key => {
        const element = document.getElementById(key);
        if (element) {
            if (element.type === 'checkbox') {
                element.checked = defaults[key];
            } else {
                element.value = defaults[key];
            }

            // Actualizar display de colores
            if (element.type === 'color') {
                const displayElement = document.getElementById(key + 'Value');
                if (displayElement) {
                    displayElement.textContent = defaults[key].toUpperCase();
                }
            }
        }
    });

    updatePreview();
    saveConfig();

    console.log('🔄 Valores por defecto restaurados');
}

7. Validación de Formularios

/**
 * Valida los campos del formulario
 */
function validateForm() {
    let isValid = true;
    const errors = [];

    // 1. Validar campo requerido
    const messageText = document.getElementById('messageText').value.trim();
    if (!messageText) {
        errors.push('El mensaje principal es requerido');
        isValid = false;
    }

    // 2. Validar longitud
    if (messageText.length > 250) {
        errors.push('El mensaje no puede exceder 250 caracteres');
        isValid = false;
    }

    // 3. Validar URL
    const linkUrl = document.getElementById('linkUrl').value.trim();
    if (linkUrl && !isValidUrl(linkUrl)) {
        errors.push('La URL no es válida');
        isValid = false;
    }

    // 4. Validar formato de clase CSS
    const iconClass = document.getElementById('iconClass').value.trim();
    if (iconClass && !/^[\w\s-]+$/.test(iconClass)) {
        errors.push('La clase del icono contiene caracteres inválidos');
        isValid = false;
    }

    if (!isValid) {
        alert('⚠️ Errores de validación:\n\n' + errors.join('\n'));
    }

    return isValid;
}

/**
 * Valida si una URL es válida
 */
function isValidUrl(string) {
    // Permitir rutas relativas
    if (string.startsWith('/')) {
        return true;
    }

    // Validar URLs absolutas
    try {
        new URL(string);
        return true;
    } catch (_) {
        return false;
    }
}

8. Sistema de Notificaciones

/**
 * Muestra notificación temporal
 */
function showNotification(message, type = 'success') {
    const notification = document.createElement('div');
    notification.className = `alert alert-${type === 'success' ? 'success' : 'danger'} position-fixed top-0 start-50 translate-middle-x mt-3`;
    notification.style.zIndex = '9999';
    notification.innerHTML = `
        <i class="bi bi-${type === 'success' ? 'check-circle' : 'exclamation-circle'} me-2"></i>
        ${message}
    `;

    document.body.appendChild(notification);

    setTimeout(() => {
        notification.remove();
    }, 3000);
}

// Uso
showNotification('Cambios guardados', 'success');
showNotification('Error al guardar cambios', 'error');

9. Auto-save (Opcional)

/**
 * Auto-guarda cada vez que cambia un campo
 */
function initializeAutoSave() {
    const fields = document.querySelectorAll('input, select, textarea');

    fields.forEach(field => {
        field.addEventListener('change', function() {
            saveConfig();
            console.log('💾 Auto-guardado');
        });
    });
}

// Llamar después de initializeEventListeners()

Volver al Índice

← Volver al README