Exploración de cursos por categorías
- Impacto — Tema:
classes/output/core/course_renderer.php(override decourse_category(),coursecat_category(),coursecat_category_content()ycoursecat_coursebox()), templatescoursecategory_card.mustache,coursecategory_subcat.mustache,coursecategory_courserow.mustachey extensión decourse_search_hero.mustache, SCSSscss/coursecategories.scss, AMDcoursecategories.js - Impacto — Plugin: —
- Requisito: Diseño (Figma) · Figma:
19674-15647(encabezado "Explora todos los cursos" con selector + buscador),19674-15652(card de categoría, colapsada y expandida) · Estado: Implementado
Propósito
Sustituye el listado plano de categorías que renderiza core en /course/index.php
(core_course_renderer::course_category(), árbol de <div class="category"> con
html_writer puro) por el mismo lenguaje visual ya usado en
la búsqueda de cursos: un encabezado ilustrado con
selector de categoría y buscador, y una card por categoría que se expande in situ para
mostrar sus subcategorías y cursos.
- Encabezado compartido con
/course/search.php. Misma tarjeta ilustrada ("Explora todos los cursos" + subtítulo), ahora con el selector "Todas las categorías" que esa página dejó pendiente (ver Selector de categoría descartado en esta iteración de esa ficha) — aquí sí es viable porque es navegación directa, no un filtro combinado con búsqueda de texto libre. - Card de categoría con portada (extraída de la descripción de la categoría), etiqueta "Categoría", nombre, contador "N Cursos" y chevron que expande/colapsa.
- Listado expandido con borde y esquinas redondeadas, filas de curso (enlace directo) y filas de subcategoría (en negrita, expandibles a su vez, con el mismo mecanismo, cualquier nivel de profundidad).
- Reutiliza el expansor AJAX de core (
moodle-course-categoryexpander,course/category.ajax.php) sin reimplementarlo: solo cambia el markup que ese mecanismo muestra y oculta. - Ancho de página ampliado a 1087px (
#page.drawers .main-inner, mismo patrón ya usado en "Mis cursos" y en/course/search.php): ellimitedwidthde core lo deja en 1024px, por debajo de los 1038-1041px que miden el hero y la card en el Figma.
Por qué un renderer y no solo CSS
El markup de core (coursecat_category(), coursecat_coursebox(), ambos en
course/renderer.php) no tiene portada, etiqueta ni tarjeta — es una lista con
indentación y un ícono de flecha de fondo. La card, el listado con borde y las filas de
curso/subcategoría del Figma no tienen equivalente en el markup de core, así que se
sobreescriben los métodos que construyen ese árbol.
Dónde vive el renderer nuevo
Reutiliza el mismo renderer namespaced que ya soporta
la card de búsqueda
(theme_cdigital\output\core\course_renderer, que extiende la clase legacy
theme_cdigital_core_course_renderer y no core_course_renderer directamente, por la
misma razón ya documentada ahí). No fue necesario crear ningún renderer adicional.
Guarda por pagetype — con una trampa
/course/index.php fija $PAGE->set_pagetype('course-index-category') (en las dos ramas,
con y sin categoryid). Pero la expansión AJAX entra por
course/category.ajax.php, que no llama a set_pagetype(), así que
initialise_default_pagetype() (lib/pagelib.php) deriva el pagetype del script:
solo quita el sufijo literal .php y cambia / por -, sin tocar los demás puntos, así
que course/category.ajax.php se convierte en course-category.ajax (con un punto
antes de "ajax", no un guion). Se verificó en vivo: con la guarda mal escrita
(course-category-ajax, guion) el contenido cargado por AJAX volvía al markup de core
(<a class="aalink"> plano) mientras la carga inicial de la página ya mostraba el diseño
nuevo — el bug solo aparecía al expandir una card. Los métodos que dependen de esta guarda
aceptan ambos pagetypes vía la constante CATEGORYBROWSE_PAGETYPES.
course_category() no necesita guarda: /course/index.php:68 es su único invocador en
todo el código base (mismo criterio ya aplicado a search_courses()).
Datos de la card
| Campo | Origen |
|---|---|
| Portada | Primera <img> de la descripción de la categoría (coursecat_helper::get_category_formatted_description(), parseada con DOMDocument), con fallback a pix/no-image.jpg |
| Nombre | core_course_category::get_formatted_name() |
| Contador "N Cursos" | core_course_category::get_courses_count() — cursos directos, no recursivo |
| Selector de categorías | core_course_category::make_categories_list(), con una opción "Todas las categorías" añadida al inicio apuntando a /course/index.php sin parámetro |
La portada no es un campo nuevo de la categoría. Moodle no tiene imagen dedicada de
categoría; se reutiliza la descripción (Administración del sitio → Cursos → Gestionar
categorías → Editar categoría → subir la ilustración dentro del editor de descripción). El
texto de la descripción en sí no se muestra en la card — solo se extrae la imagen. La
descripción completa (con la imagen incluida) sí se sigue mostrando, sin cambios, cuando se
navega a una categoría concreta (?categoryid=N), igual que hacía core.
Por qué la card no usa un enlace ni una imagen real
moodle-course-categoryexpander (course/yui/src/categoryexpander) delega los clics a
.category .info .categoryname para expandir/contraer, y ignora cualquier clic cuyo
objetivo sea un <a> o una <img> (categoryexpander.js, comprobación al inicio del
handler). Por eso:
- La portada es un
<div>conbackground-image, no un<img>. - El nombre de la categoría no es un enlace — la card entera es la superficie clicable
(
role="button",tabindex="0"), y la navegación a una categoría concreta queda cubierta por el selector del encabezado (ver Desviaciones).
Las filas de curso, en cambio, sí son <a> normales: no forman parte de la delegación de
moodle-course-categoryexpander (que solo vigila .categoryname y .coursebox .moreinfo,
ninguno de los cuales usa esta fila), así que no hay conflicto.
Cards, filas y profundidad
El árbol de categorías de core es recursivo por profundidad
(coursecat_category($chelper, $coursecat, $depth)); el tema decide el markup según
$depth:
$depth <= 1→ card completa (coursecategory_card.mustache): portada, etiqueta, nombre, contador.$depth >= 2→ fila compacta (coursecategory_subcat.mustache): solo nombre en negrita y chevron, sin portada ni contador — así se ve tanto una subcategoría de primer nivel como cualquier nivel más profundo, expandible de la misma forma.
El listado expandido de una card (coursecat_category_content()) se envuelve en una caja
con borde redondeado (cdigital-catcard__list) siempre que $depth >= 1 — es decir,
cuando lo que va a mostrar son filas compactas, no cards. La vista de "todas las
categorías" ($depth == 0, pseudo-categoría raíz) y la vista de una categoría concreta
navegada directamente (?categoryid=N, también $depth == 0 para ese árbol) no se
envuelven: en ambos casos lo siguiente que se muestra son cards de primer nivel, que ya
aportan su propio borde — envolverlas también habría duplicado el recuadro.
Desviaciones conscientes respecto al Figma
- Nombre de categoría no enlazado. Core lo emite como enlace a
/course/index.php?categoryid=…; aquí el chevron es la interacción principal (expandir), y el expansor de core ignora los clics sobre<a>. La navegación directa a una categoría queda cubierta por el selector del encabezado — mismo criterio ya aplicado al chip de categoría en la card de búsqueda. - Vista de una categoría concreta (
?categoryid=N) sin card contenedora. El Figma solo especifica la vista de "todas las categorías". Al navegar a una categoría, sus subcategorías se muestran como cards (mismo componente, un nivel más adentro) y sus cursos directos como filas sueltas, sin la caja con borde que normalmente las envuelve (ver Cards, filas y profundidad) — no hay spec de Figma para esta variante. - Enlace "Expandir todo / Contraer todo" (
coursecat_tree(), solo aparece si la categoría tiene hijos): no está en el Figma. Se conserva por funcionalidad y accesibilidad, con un estilo discreto. - Menú "Más acciones" (
core_course\output\category_action_bar, visible solo para quien puede crear cursos o gestionar categorías): no está en el Figma, se conserva dentro del encabezado por funcionalidad. - Contador "N Cursos": cuenta cursos directos de la categoría, no recursivo (mismo
comportamiento que
core_course_category::get_courses_count()sin opciones).
Archivos clave
| Archivo | Descripción |
|---|---|
theme/cdigital/classes/output/core/course_renderer.php | Override de course_category() (hero + selector, sin guarda), coursecat_category()/coursecat_category_content()/coursecat_coursebox() (con guarda CATEGORYBROWSE_PAGETYPES), y category_cover_url() (extracción de portada) |
theme/cdigital/templates/coursecategory_card.mustache | Markup de la card de categoría de primer nivel |
theme/cdigital/templates/coursecategory_subcat.mustache | Markup de la fila de subcategoría anidada |
theme/cdigital/templates/coursecategory_courserow.mustache | Markup de la fila de curso |
theme/cdigital/templates/course_search_hero.mustache | Ampliado con categoryselect/additionaloptions opcionales (sin efecto en /course/search.php, que no los pasa) |
theme/cdigital/scss/coursecategories.scss | Card, listado con borde, filas de curso/subcategoría; también aloja ahora el bloque de hero compartido con /course/search.php (antes vivía solo en coursesearch.scss) |
theme/cdigital/amd/src/coursecategories.js | Sincroniza aria-expanded en la cabecera clicable a partir de la clase collapsed que ya gestiona moodle-course-categoryexpander (sin registrar listeners de clic propios, mismo patrón que mycourses_filters.js) |
theme/cdigital/lang/{es,en}/theme_cdigital.php | Strings coursecategory_herotitle, coursecategory_herosubtitle, coursecategory_allcategories, coursecategory_categorytag, coursecategory_coursecount(one) |
Build y despliegue
Hay un módulo AMD nuevo (coursecategories.js): requiere grunt amd antes de desplegar.
El SCSS lo compila Moodle en runtime. Para el despliegue completo (copia de código,
upgrade, purga de cachés), ver Procedimiento de despliegue.