repository = new WordPressSnippetRepository($wpdb); $this->getAllUseCase = new GetAllSnippetsUseCase($this->repository); // NOTA: El handler POST está en CustomCSSManagerBootstrap (admin_init) } /** * Construye el formulario del componente * * @param string $componentId ID del componente (custom-css-manager) * @return string HTML del formulario */ public function buildForm(string $componentId): string { $snippets = $this->getAllUseCase->execute(); $message = $this->getFlashMessage(); $html = ''; // Header $html .= $this->buildHeader($componentId, count($snippets)); // Toast para mensajes (usa el sistema existente de admin-dashboard.js) if ($message) { $html .= $this->buildToastTrigger($message); } // Lista de snippets existentes $html .= $this->buildSnippetsList($snippets); // Formulario de creación/edición $html .= $this->buildSnippetForm(); // JavaScript $html .= $this->buildJavaScript(); return $html; } /** * Construye el header del componente */ private function buildHeader(string $componentId, int $snippetCount): string { $html = '
'; $html .= '
'; $html .= '

Gestor de CSS Personalizado

'; $html .= ' ' . esc_html($snippetCount) . ' snippet(s)'; $html .= '
'; $html .= '
'; $html .= '

Gestiona snippets de CSS personalizados. Los snippets críticos se cargan en el head, los diferidos en el footer.

'; $html .= '
'; $html .= '
'; return $html; } /** * Construye la lista de snippets existentes */ private function buildSnippetsList(array $snippets): string { $html = '
'; $html .= '
'; $html .= '
'; $html .= ' '; $html .= ' Snippets Configurados'; $html .= '
'; if (empty($snippets)) { $html .= '

No hay snippets configurados.

'; } else { $html .= '
'; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; $html .= ' '; foreach ($snippets as $snippet) { $html .= $this->renderSnippetRow($snippet); } $html .= ' '; $html .= '
NombreTipoPáginasEstadoAcciones
'; $html .= '
'; } $html .= '
'; $html .= '
'; return $html; } /** * Renderiza una fila de snippet en la tabla */ private function renderSnippetRow(array $snippet): string { $id = esc_attr($snippet['id']); $name = esc_html($snippet['name']); $type = $snippet['type'] === 'critical' ? 'Crítico' : 'Diferido'; $typeBadge = $snippet['type'] === 'critical' ? 'bg-danger' : 'bg-info'; $pages = implode(', ', $snippet['pages'] ?? ['all']); $enabled = ($snippet['enabled'] ?? false) ? 'Activo' : 'Inactivo'; $enabledBadge = ($snippet['enabled'] ?? false) ? 'bg-success' : 'bg-secondary'; // Usar data-attribute para JSON seguro $snippetJson = esc_attr(wp_json_encode($snippet, JSON_HEX_APOS | JSON_HEX_QUOT)); $nonce = wp_create_nonce(self::NONCE_ACTION); return << {$name} {$type} {$pages} {$enabled}
HTML; } /** * Construye el formulario de creación/edición de snippets */ private function buildSnippetForm(): string { $nonce = wp_create_nonce(self::NONCE_ACTION); $html = '
'; $html .= '
'; $html .= '
'; $html .= ' '; $html .= ' Agregar/Editar Snippet'; $html .= '
'; $html .= '
'; $html .= ' '; $html .= ' '; $html .= ' '; $html .= '
'; // Nombre $html .= '
'; $html .= ' '; $html .= ' '; $html .= '
'; // Tipo $html .= '
'; $html .= ' '; $html .= ' '; $html .= '
'; // Orden $html .= '
'; $html .= ' '; $html .= ' '; $html .= '
'; // Descripción $html .= '
'; $html .= ' '; $html .= ' '; $html .= '
'; // Páginas $html .= '
'; $html .= ' '; $html .= '
'; foreach ($this->getPageOptions() as $value => $label) { $checked = $value === 'all' ? 'checked' : ''; $html .= sprintf( '
', esc_attr($value), esc_attr($value), $checked, esc_attr($value), esc_html($label) ); } $html .= '
'; $html .= '
'; // Estado $html .= '
'; $html .= ' '; $html .= '
'; $html .= ' '; $html .= ' '; $html .= '
'; $html .= '
'; // Código CSS $html .= '
'; $html .= ' '; $html .= ' '; $html .= ' Crítico: máx 14KB | Diferido: máx 100KB'; $html .= '
'; // Botones $html .= '
'; $html .= ' '; $html .= ' '; $html .= '
'; $html .= '
'; $html .= '
'; $html .= '
'; $html .= '
'; return $html; } /** * Genera el JavaScript necesario para el formulario */ private function buildJavaScript(): string { return << // Event delegation para botones de edición document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.btn-edit-snippet').forEach(function(btn) { btn.addEventListener('click', function() { const snippet = JSON.parse(this.dataset.snippet); editCssSnippet(snippet); }); }); }); function editCssSnippet(snippet) { document.getElementById('snippet_id').value = snippet.id; document.getElementById('snippet_name').value = snippet.name; document.getElementById('snippet_description').value = snippet.description || ''; document.getElementById('snippet_type').value = snippet.type; document.getElementById('snippet_order').value = snippet.order || 100; document.getElementById('snippet_css').value = snippet.css; document.getElementById('snippet_enabled').checked = snippet.enabled; // Actualizar checkboxes de páginas document.querySelectorAll('input[name="snippet_pages[]"]').forEach(cb => { cb.checked = (snippet.pages || ['all']).includes(cb.value); }); document.getElementById('snippet_name').focus(); // Scroll al formulario document.getElementById('roi-snippet-form').scrollIntoView({ behavior: 'smooth' }); } function resetCssForm() { document.getElementById('roi-snippet-form').reset(); document.getElementById('snippet_id').value = ''; } JS; } /** * Opciones de páginas disponibles */ private function getPageOptions(): array { return [ 'all' => 'Todas', 'home' => 'Inicio', 'posts' => 'Posts', 'pages' => 'Páginas', 'archives' => 'Archivos', ]; } /** * Obtiene mensaje flash de la URL */ private function getFlashMessage(): ?array { $message = $_GET['roi_message'] ?? null; if ($message === 'success') { return ['type' => 'success', 'text' => 'Cambios guardados correctamente']; } if ($message === 'error') { $error = urldecode($_GET['roi_error'] ?? 'Error desconocido'); return ['type' => 'error', 'text' => $error]; } return null; } /** * Genera script para mostrar Toast */ private function buildToastTrigger(array $message): string { $type = esc_js($message['type']); $text = esc_js($message['text']); // Mapear tipo a configuración de Bootstrap $typeMap = [ 'success' => ['bg' => 'success', 'icon' => 'bi-check-circle-fill'], 'error' => ['bg' => 'danger', 'icon' => 'bi-x-circle-fill'], ]; $config = $typeMap[$type] ?? $typeMap['success']; $bg = $config['bg']; $icon = $config['icon']; return << document.addEventListener('DOMContentLoaded', function() { // Crear container de toasts si no existe let toastContainer = document.getElementById('roiToastContainer'); if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'roiToastContainer'; toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x'; toastContainer.style.top = '60px'; toastContainer.style.zIndex = '999999'; document.body.appendChild(toastContainer); } // Crear toast const toastId = 'toast-' + Date.now(); const toastHTML = ` `; toastContainer.insertAdjacentHTML('beforeend', toastHTML); // Mostrar toast const toastElement = document.getElementById(toastId); const toast = new bootstrap.Toast(toastElement, { autohide: true, delay: 5000 }); toast.show(); // Eliminar del DOM después de ocultarse toastElement.addEventListener('hidden.bs.toast', function() { toastElement.remove(); }); // Limpiar parámetros de URL sin recargar const url = new URL(window.location.href); url.searchParams.delete('roi_message'); url.searchParams.delete('roi_error'); window.history.replaceState({}, '', url.toString()); }); HTML; } }