299 lines
9.9 KiB
Rust
299 lines
9.9 KiB
Rust
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<Html>),
|
|
/// Elemento que despliega un menú [`Dropdown`].
|
|
Dropdown(Typed<Dropdown>),
|
|
}
|
|
|
|
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<String> {
|
|
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 `<li>`
|
|
/// 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<str>) -> 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<str>) -> Self {
|
|
self.classes.alter_value(op, classes);
|
|
self
|
|
}
|
|
}
|