use pagetop::prelude::*;
use crate::tree::{MenuKey, MenuNode, TreeOptions, build_tree, try_resolve_menu_url};
/// Renderiza un menú completo como bloque de navegación HTML.
///
/// Se construye componiendo [`Nav`] y [`nav::Item`] -- con [`Dropdown`]/[`dropdown::Item`] para
/// los nodos con hijos -- a partir del árbol del menú, así que produce el mismo marcado accesible
/// que cualquier `Nav`/`Dropdown` y se beneficia igual del CSS/JavaScript que aporta el tema
/// activo:
///
/// ```html
///
/// ```
///
/// Los temas pueden sobreescribir el render con `handle_component()`, tanto de `MenuBlock` como,
/// más generalmente, de [`Nav`]/[`nav::Item`]/[`Dropdown`]/[`dropdown::Item`]. `pagetop-bootsier`
/// ya intercepta `Dropdown` así (ver `theme::bs::dropdown`), y por tanto también los que cuelguen
/// de un `nav::Item::dropdown()`; `MenuBlock`, `Nav` y `Navbar` siguen sin interceptarse: Bootsier
/// mantiene sus propios `bs::Navbar`/`bs::Nav`, sin relación con este componente.
///
/// # Limitaciones conocidas
///
/// - **Profundidad máxima de 2 niveles.** [`Dropdown`] no admite submenús anidados (como
/// Bootstrap, del que toma su marcado): un nodo de tercer nivel o más profundo nunca se
/// construye -- [`TreeOptions::max_depth`] se acota internamente a `2` con independencia de lo
/// que indique [`with_max_depth()`](Self::with_max_depth), así que no hay pérdida silenciosa de
/// datos, sencillamente no se piden a la base de datos.
/// - **Sin colapso responsive propio.** A diferencia del antiguo `Menu::collapsible`, `Nav` es una
/// lista plana sin botón ni JavaScript de colapso; una aplicación que necesite ese
/// comportamiento debe envolver `MenuBlock` en su propia chrome hasta que exista un componente
/// `Navbar` en el core.
/// - `MenuNode::in_active_trail` y `MenuNode::expanded` (ver [`crate::tree::MenuNode`]) todavía no
/// se reflejan en el marcado -- ni [`nav::Item`] ni [`dropdown::Item`] tienen hoy una forma
/// verificada de pre-abrirse en el servidor sin que la mejora progresiva del tema
/// (`accessible-menu` en `Basic`) lo sobrescriba al inicializarse. Sólo se traduce el estado
/// `is_active`/`enabled` de cada nodo.
///
/// # Ejemplo
///
/// ```rust,no_run
/// use pagetop::prelude::*;
/// use pagetop_menu::component::MenuBlock;
///
/// async fn handler(request: HttpRequest) -> Result {
/// Page::new(request)
/// .with_child(MenuBlock::with("main"))
/// .render().await
/// }
/// ```
#[derive(AutoDefault, Clone, Debug)]
pub struct MenuBlock {
menu_name: Option,
show_title: bool,
max_depth: Option,
include_disabled: bool,
hide_when_empty: bool,
}
#[async_trait]
impl Component for MenuBlock {
fn new() -> Self {
MenuBlock {
hide_when_empty: true,
..Self::default()
}
}
async fn prepare(&self, cx: &mut Context) -> Result {
let Some(name) = &self.menu_name else {
return Ok(html! {});
};
// `Dropdown` no admite submenús: nunca se piden más de 2 niveles al árbol.
let opts = TreeOptions {
max_depth: Some(self.max_depth.map(|d| d.min(2)).unwrap_or(2)),
include_disabled: self.include_disabled,
};
let Some(tree) = build_tree(MenuKey::Name(name.clone()), cx, &opts).await else {
return Ok(html! {});
};
if tree.roots.is_empty() && self.hide_when_empty {
return Ok(html! {});
}
let mut nav = Nav::new();
for node in &tree.roots {
nav = nav.with_item(node_to_item(node, cx));
}
let aria_label = (!tree.title.is_empty()).then_some(&tree.title);
Ok(html! {
@if self.show_title {
h2.menu-title { (&tree.title) }
}
nav aria-label=[aria_label] {
(nav.render(cx).await)
}
})
}
}
#[builder_impl]
impl MenuBlock {
/// Crea un `MenuBlock` para el menú con el `machine_name` dado.
pub fn with(menu_name: impl Into) -> Self {
let mut block = Self::new();
block.menu_name = Some(menu_name.into());
block
}
pub fn with_show_title(mut self, v: impl Into