use pagetop::prelude::*; use crate::prelude::*; use crate::LOCALES_BOOTSIER; // **< ItemKind >*********************************************************************************** /// Tipos de [`nav::Item`](crate::theme::nav::Item) disponibles en un menú /// [`Nav`](crate::theme::Nav). /// /// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar /// con él. #[derive(AutoDefault)] pub enum ItemKind { /// Elemento vacío, no produce salida. #[default] Void, /// Etiqueta sin comportamiento interactivo. Label(L10n), /// Elemento de navegación basado en una [`RoutePath`] dinámica devuelta por /// [`FnPathByContext`]. Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente /// deshabilitado. Link { label: L10n, route: FnPathByContext, blank: bool, disabled: bool, }, /// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del /// menú, sin añadir ningún comportamiento de navegación adicional. Html(Typed), /// Elemento que despliega un menú [`Dropdown`]. Dropdown(Typed), } impl ItemKind { const ITEM: &str = "nav-item"; const DROPDOWN: &str = "nav-item dropdown"; // Devuelve las clases base asociadas al tipo de elemento. #[inline] const fn as_str(&self) -> &'static str { match self { Self::Void => "", Self::Dropdown(_) => Self::DROPDOWN, _ => Self::ITEM, } } /* Añade las clases asociadas al tipo de elemento a la cadena de clases (reservado). #[inline] pub(crate) fn push_class(&self, classes: &mut String) { let class = self.as_str(); if class.is_empty() { return; } if !classes.is_empty() { classes.push(' '); } classes.push_str(class); } */ // Devuelve las clases asociadas al tipo de elemento. #[inline] pub(crate) fn to_class(&self) -> String { self.as_str().to_owned() } } // **< Item >*************************************************************************************** /// Representa un **elemento individual** de un menú [`Nav`](crate::theme::Nav). /// /// Cada instancia de [`nav::Item`](crate::theme::nav::Item) se traduce en un componente visible que /// puede comportarse como texto, enlace, contenido HTML o menú desplegable, según su [`ItemKind`]. /// /// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. #[derive(AutoDefault, Getters)] pub struct Item { #[getters(skip)] id: AttrId, /// Devuelve las clases CSS asociadas al elemento. classes: AttrClasses, /// Devuelve el tipo de elemento representado. item_kind: ItemKind, } impl Component for Item { fn new() -> Self { Item::default() } fn id(&self) -> Option { self.id.get() } fn setup_before_prepare(&mut self, _cx: &mut Context) { self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class()); } fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { match self.item_kind() { ItemKind::Void => PrepareMarkup::None, ItemKind::Label(label) => PrepareMarkup::With(html! { li id=[self.id()] class=[self.classes().get()] { span class="nav-link disabled" aria-disabled="true" { (label.using(cx)) } } }), ItemKind::Link { label, route, blank, disabled, } => { let route_link = route(cx); let current_path = cx.request().map(|request| request.path()); let is_current = !*disabled && (current_path == Some(route_link.path())); let mut classes = "nav-link".to_string(); if is_current { classes.push_str(" active"); } if *disabled { classes.push_str(" disabled"); } let href = (!*disabled).then_some(route_link); let target = (!*disabled && *blank).then_some("_blank"); let rel = (!*disabled && *blank).then_some("noopener noreferrer"); let aria_current = (href.is_some() && is_current).then_some("page"); let aria_disabled = (*disabled).then_some("true"); PrepareMarkup::With(html! { li id=[self.id()] class=[self.classes().get()] { a class=(classes) href=[href] target=[target] rel=[rel] aria-current=[aria_current] aria-disabled=[aria_disabled] { (label.using(cx)) } } }) } ItemKind::Html(html) => PrepareMarkup::With(html! { li id=[self.id()] class=[self.classes().get()] { (html.render(cx)) } }), ItemKind::Dropdown(menu) => { if let Some(dd) = menu.borrow() { let items = dd.items().render(cx); if items.is_empty() { return PrepareMarkup::None; } let title = dd.title().lookup(cx).unwrap_or_else(|| { L10n::t("dropdown", &LOCALES_BOOTSIER) .lookup(cx) .unwrap_or_else(|| "Dropdown".to_string()) }); PrepareMarkup::With(html! { li id=[self.id()] class=[self.classes().get()] { a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false" { (title) } ul class="dropdown-menu" { (items) } } }) } else { PrepareMarkup::None } } } } } impl Item { /// Crea un elemento de tipo texto, mostrado sin interacción. pub fn label(label: L10n) -> Self { Item { item_kind: ItemKind::Label(label), ..Default::default() } } /// Crea un enlace para la navegación. /// /// La ruta se obtiene invocando [`FnPathByContext`], que devuelve dinámicamente una /// [`RoutePath`] en función del [`Context`]. El enlace se marca como `active` si la ruta actual /// del *request* coincide con la ruta de destino (devuelta por `RoutePath::path`). pub fn link(label: L10n, route: FnPathByContext) -> Self { Item { item_kind: ItemKind::Link { label, route, blank: false, disabled: false, }, ..Default::default() } } /// Crea un enlace deshabilitado que no permite la interacción. pub fn link_disabled(label: L10n, route: FnPathByContext) -> Self { Item { item_kind: ItemKind::Link { label, route, blank: false, disabled: true, }, ..Default::default() } } /// Crea un enlace que se abre en una nueva ventana o pestaña. pub fn link_blank(label: L10n, route: FnPathByContext) -> Self { Item { item_kind: ItemKind::Link { label, route, blank: true, disabled: false, }, ..Default::default() } } /// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana. pub fn link_blank_disabled(label: L10n, route: FnPathByContext) -> Self { Item { item_kind: ItemKind::Link { label, route, blank: true, disabled: true, }, ..Default::default() } } /// Crea un elemento con contenido HTML arbitrario. /// /// El contenido se renderiza tal cual lo devuelve el componente [`Html`], dentro de un `
  • ` /// con las clases de navegación asociadas a [`Item`]. pub fn html(html: Html) -> Self { Item { item_kind: ItemKind::Html(Typed::with(html)), ..Default::default() } } /// Crea un elemento de navegación que contiene un menú desplegable [`Dropdown`]. /// /// Sólo se tienen en cuenta **el título** (si no existe, se asigna uno por defecto) y **la /// lista de elementos** del [`Dropdown`]; el resto de propiedades del componente no afectarán /// a su representación en [`Nav`]. pub fn dropdown(menu: Dropdown) -> Self { Item { item_kind: ItemKind::Dropdown(Typed::with(menu)), ..Default::default() } } // **< Item BUILDER >*************************************************************************** /// Establece el identificador único (`id`) del elemento. #[builder_fn] pub fn with_id(mut self, id: impl AsRef) -> Self { self.id.alter_value(id); self } /// Modifica la lista de clases CSS aplicadas al elemento. #[builder_fn] pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { self.classes.alter_value(op, classes); self } }