- 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>
549 lines
15 KiB
Markdown
549 lines
15 KiB
Markdown
# 💻 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)
|