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

549 lines
15 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 💻 JAVASCRIPT PATTERNS
## 1. Inicialización del Componente
```javascript
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
```javascript
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
```javascript
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
```javascript
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
```javascript
function initializeResetButton() {
const resetBtn = document.getElementById('resetDefaults');
if (resetBtn) {
resetBtn.addEventListener('click', resetToDefaults);
}
}
```
---
## 3. Función updatePreview()
```javascript
/**
* 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)
```javascript
/**
* 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)
```javascript
/**
* 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
```javascript
/**
* 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
```javascript
/**
* 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
```javascript
/**
* 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
```javascript
/**
* 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
```javascript
/**
* 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)
```javascript
/**
* 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](README.md)