🚧 [bootsier] Refina el funcionamiento de Navbar

This commit is contained in:
Manuel Cillero 2025-01-05 23:38:01 +01:00
parent ee84c219cc
commit 6cb67027f6
6 changed files with 204 additions and 173 deletions

View file

@ -18,7 +18,7 @@ pub use offcanvas::{
// Navbar. // Navbar.
pub mod navbar; pub mod navbar;
pub use navbar::{Navbar, NavbarType}; pub use navbar::{Navbar, NavbarContent, NavbarToggler};
/// Define los puntos de interrupción (*breakpoints*) usados por Bootstrap para diseño responsivo. /// Define los puntos de interrupción (*breakpoints*) usados por Bootstrap para diseño responsivo.
#[rustfmt::skip] #[rustfmt::skip]
@ -37,10 +37,9 @@ pub enum BreakPoint {
} }
impl BreakPoint { impl BreakPoint {
/// Indica si el punto de interrupción es efectivo en Bootstrap. /// Verifica si es un punto de interrupción efectivo en Bootstrap.
/// ///
/// Devuelve `true` si el valor es `SM`, `MD`, `LG`, `XL` o `XXL`. /// Devuelve `true` si el valor es `SM`, `MD`, `LG`, `XL` o `XXL`. Y `false` en otro caso.
/// Devuelve `false` si es `None`, `Fluid` o `FluidMax`.
pub fn is_breakpoint(&self) -> bool { pub fn is_breakpoint(&self) -> bool {
!matches!( !matches!(
self, self,
@ -50,8 +49,8 @@ impl BreakPoint {
/// Genera un nombre de clase CSS basado en el punto de interrupción. /// Genera un nombre de clase CSS basado en el punto de interrupción.
/// ///
/// Si es un punto de interrupción efectivo (ver [`is_breakpoint()`] se concatenan el prefijo /// Si es un punto de interrupción efectivo (ver [`is_breakpoint()`] se concatena el prefijo
/// proporcionado, un guion (`-`) y el texto asociado al punto de interrupción. En otro caso, se /// proporcionado, un guion (`-`) y el texto asociado al punto de interrupción. En otro caso
/// devuelve únicamente el prefijo. /// devuelve únicamente el prefijo.
/// ///
/// # Parámetros /// # Parámetros
@ -62,14 +61,14 @@ impl BreakPoint {
/// ///
/// ```rust#ignore /// ```rust#ignore
/// let breakpoint = BreakPoint::MD; /// let breakpoint = BreakPoint::MD;
/// let class = breakpoint.breakpoint_class("col"); /// let class = breakpoint.to_class("col");
/// assert_eq!(class, "col-md".to_string()); /// assert_eq!(class, "col-md".to_string());
/// ///
/// let breakpoint = BreakPoint::Fluid; /// let breakpoint = BreakPoint::Fluid;
/// let class = breakpoint.breakpoint_class("offcanvas"); /// let class = breakpoint.to_class("offcanvas");
/// assert_eq!(class, "offcanvas".to_string()); /// assert_eq!(class, "offcanvas".to_string());
/// ``` /// ```
pub fn breakpoint_class(&self, prefix: impl Into<String>) -> String { pub fn to_class(&self, prefix: impl Into<String>) -> String {
let prefix: String = prefix.into(); let prefix: String = prefix.into();
if self.is_breakpoint() { if self.is_breakpoint() {
join_string!(prefix, "-", self.to_string()) join_string!(prefix, "-", self.to_string())
@ -77,6 +76,41 @@ impl BreakPoint {
prefix prefix
} }
} }
/// Intenta generar un nombre de clase CSS basado en el punto de interrupción.
///
/// Si es un punto de interrupción efectivo (ver [`is_breakpoint()`] se concatena el prefijo
/// proporcionado, un guion (`-`) y el texto asociado al punto de interrupción. En otro caso,
/// devuelve `None`.
///
/// # Parámetros
///
/// - `prefix`: Prefijo a concatenar con el punto de interrupción.
///
/// # Retorno
///
/// - `Some(String)`: Si es un punto de interrupción efectivo.
/// - `None`: En otro caso.
///
/// # Ejemplo
///
/// ```rust#ignore
/// let breakpoint = BreakPoint::MD;
/// let class = breakpoint.try_class("col");
/// assert_eq!(class, Some("col-md".to_string()));
///
/// let breakpoint = BreakPoint::Fluid;
/// let class = breakpoint.try_class("navbar-expanded");
/// assert_eq!(class, None);
/// ```
pub fn try_class(&self, prefix: impl Into<String>) -> Option<String> {
let prefix: String = prefix.into();
if self.is_breakpoint() {
Some(join_string!(prefix, "-", self.to_string()))
} else {
None
}
}
} }
/// Devuelve el texto asociado al punto de interrupción usado por Bootstrap. /// Devuelve el texto asociado al punto de interrupción usado por Bootstrap.

View file

@ -1,8 +1,5 @@
mod component; mod component;
pub use component::{Navbar, NavbarType}; pub use component::{Navbar, NavbarContent, NavbarToggler};
mod element;
pub use element::{Element, ElementType};
mod nav; mod nav;
pub use nav::Nav; pub use nav::Nav;

View file

@ -4,22 +4,33 @@ use crate::bs::navbar;
use crate::bs::{BreakPoint, Offcanvas}; use crate::bs::{BreakPoint, Offcanvas};
use crate::LOCALES_BOOTSIER; use crate::LOCALES_BOOTSIER;
const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas";
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub enum NavbarType { pub enum NavbarToggler {
#[default] #[default]
Default, Enabled,
Basic, Disabled,
}
#[derive(AutoDefault)]
pub enum NavbarContent {
#[default]
None,
Nav(Typed<navbar::Nav>),
Offcanvas(Typed<Offcanvas>), Offcanvas(Typed<Offcanvas>),
Text(L10n),
} }
#[rustfmt::skip] #[rustfmt::skip]
#[derive(AutoDefault)] #[derive(AutoDefault)]
pub struct Navbar { pub struct Navbar {
id : OptionId, id : OptionId,
classes : OptionClasses, classes: OptionClasses,
navbar_type: NavbarType, expand : BreakPoint,
expand : BreakPoint, toggler: NavbarToggler,
elements : Children, content: NavbarContent,
} }
impl ComponentTrait for Navbar { impl ComponentTrait for Navbar {
@ -36,75 +47,58 @@ impl ComponentTrait for Navbar {
ClassesOp::Prepend, ClassesOp::Prepend,
[ [
"navbar".to_string(), "navbar".to_string(),
self.expand().breakpoint_class("navbar-expand"), self.expand().try_class("navbar-expand").unwrap_or_default(),
] ]
.join(" "), .join(" "),
); );
} }
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let elements = self.elements().render(cx);
if elements.is_empty() {
return PrepareMarkup::None;
}
let id = cx.required_id::<Self>(self.id()); let id = cx.required_id::<Self>(self.id());
let (output, id_content) = if let NavbarType::Offcanvas(oc) = self.navbar_type() {
(
oc.writable()
.alter_children(ChildOp::Prepend(Child::with(Html::with(elements))))
.render(cx),
cx.required_id::<Offcanvas>(oc.id()),
)
} else {
(elements, join_string!(id, "-content"))
};
let id_content_target = join_string!("#", id_content);
PrepareMarkup::With(html! { let content = match self.content() {
nav id=(id) class=[self.classes().get()] { NavbarContent::None => return PrepareMarkup::None,
div class="container-fluid" { NavbarContent::Nav(nav) => {
@match self.navbar_type() { let id_content = join_string!(id, "-content");
NavbarType::Default => { match self.toggler() {
button NavbarToggler::Enabled => self.toggler_wrapper(
type="button" TOGGLE_COLLAPSE,
class="navbar-toggler" L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid()),
data-bs-toggle="collapse" id_content,
data-bs-target=(id_content_target) nav.render(cx),
aria-controls=(id_content) ),
aria-expanded="false" NavbarToggler::Disabled => nav.render(cx),
aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid())]
{
span class="navbar-toggler-icon" {}
}
div id=(id_content) class="collapse navbar-collapse" {
(output)
}
},
NavbarType::Basic => {
(output)
},
NavbarType::Offcanvas(_) => {
button
type="button"
class="navbar-toggler"
data-bs-toggle="offcanvas"
data-bs-target=(id_content_target)
aria-controls=(id_content)
aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid())]
{
span class="navbar-toggler-icon" {}
}
(output)
},
}
} }
} }
}) NavbarContent::Offcanvas(oc) => {
let id_content = oc.id().unwrap_or_default();
self.toggler_wrapper(
TOGGLE_OFFCANVAS,
L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid()),
id_content,
oc.render(cx),
)
}
NavbarContent::Text(text) => html! {
span class="navbar-text" {
(text.escaped(cx.langid()))
}
},
};
self.nav_wrapper(id, content)
} }
} }
impl Navbar { impl Navbar {
pub fn with_nav(nav: navbar::Nav) -> Self {
Navbar::default().with_content(NavbarContent::Nav(Typed::with(nav)))
}
pub fn with_offcanvas(offcanvas: Offcanvas) -> Self {
Navbar::default().with_content(NavbarContent::Offcanvas(Typed::with(offcanvas)))
}
// Navbar BUILDER. // Navbar BUILDER.
#[fn_builder] #[fn_builder]
@ -119,12 +113,6 @@ impl Navbar {
self self
} }
#[fn_builder]
pub fn with_type(mut self, navbar_type: NavbarType) -> Self {
self.navbar_type = navbar_type;
self
}
#[fn_builder] #[fn_builder]
pub fn with_expand(mut self, bp: BreakPoint) -> Self { pub fn with_expand(mut self, bp: BreakPoint) -> Self {
self.expand = bp; self.expand = bp;
@ -132,8 +120,14 @@ impl Navbar {
} }
#[fn_builder] #[fn_builder]
pub fn with_nav(mut self, op: TypedOp<navbar::Nav>) -> Self { pub fn with_toggler(mut self, toggler: NavbarToggler) -> Self {
self.elements.alter_typed(op); self.toggler = toggler;
self
}
#[fn_builder]
pub fn with_content(mut self, content: NavbarContent) -> Self {
self.content = content;
self self
} }
@ -143,15 +137,66 @@ impl Navbar {
&self.classes &self.classes
} }
pub fn navbar_type(&self) -> &NavbarType {
&self.navbar_type
}
pub fn expand(&self) -> &BreakPoint { pub fn expand(&self) -> &BreakPoint {
&self.expand &self.expand
} }
pub fn elements(&self) -> &Children { pub fn toggler(&self) -> &NavbarToggler {
&self.elements &self.toggler
}
pub fn content(&self) -> &NavbarContent {
&self.content
}
// Navbar HELPERS.
fn nav_wrapper(&self, id: String, content: Markup) -> PrepareMarkup {
if content.is_empty() {
PrepareMarkup::None
} else {
PrepareMarkup::With(html! {
nav id=(id) class=[self.classes().get()] {
div class="container-fluid" {
(content)
}
}
})
}
}
fn toggler_wrapper(
&self,
data_bs_toggle: &str,
aria_label: Option<String>,
id_content: String,
content: Markup,
) -> Markup {
if content.is_empty() {
html! {}
} else {
let id_content_target = join_string!("#", id_content);
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
Some("false")
} else {
None
};
html! {
button
type="button"
class="navbar-toggler"
data-bs-toggle=(data_bs_toggle)
data-bs-target=(id_content_target)
aria-controls=(id_content)
aria-expanded=[aria_expanded]
aria-label=[aria_label]
{
span class="navbar-toggler-icon" {}
}
div id=(id_content) class="collapse navbar-collapse" {
(content)
}
}
}
} }
} }

View file

@ -1,63 +0,0 @@
use pagetop::prelude::*;
use crate::bs::navbar;
#[derive(AutoDefault)]
pub enum ElementType {
#[default]
None,
Nav(navbar::Nav),
Text(L10n),
}
// Element.
#[rustfmt::skip]
#[derive(AutoDefault)]
pub struct Element {
element_type: ElementType,
}
impl ComponentTrait for Element {
fn new() -> Self {
Element::default()
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
match self.element_type() {
ElementType::None => PrepareMarkup::None,
ElementType::Nav(_nav) => PrepareMarkup::With(html! {
span class="navbar-text" {
("Prueba")
}
}),
ElementType::Text(label) => PrepareMarkup::With(html! {
span class="navbar-text" {
(label.escaped(cx.langid()))
}
}),
}
}
}
impl Element {
pub fn nav(nav: navbar::Nav) -> Self {
Element {
element_type: ElementType::Nav(nav),
..Default::default()
}
}
pub fn text(label: L10n) -> Self {
Element {
element_type: ElementType::Text(label),
..Default::default()
}
}
// Element GETTERS.
pub fn element_type(&self) -> &ElementType {
&self.element_type
}
}

View file

@ -27,6 +27,9 @@ impl ComponentTrait for Item {
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
let description: Option<String> = None; let description: Option<String> = None;
// Obtiene la URL actual desde `cx.request`.
let current_path = cx.request().path();
match self.item_type() { match self.item_type() {
ItemType::Void => PrepareMarkup::None, ItemType::Void => PrepareMarkup::None,
ItemType::Label(label) => PrepareMarkup::With(html! { ItemType::Label(label) => PrepareMarkup::With(html! {
@ -38,24 +41,40 @@ impl ComponentTrait for Item {
} }
} }
}), }),
ItemType::Link(label, path) => PrepareMarkup::With(html! { ItemType::Link(label, path) => {
li class="nav-item" { let item_path = path(cx);
a class="nav-link" href=(path(cx)) title=[description] { let (class, aria) = if item_path == current_path {
//(left_icon) ("nav-item active", Some("page"))
(label.escaped(cx.langid())) } else {
//(right_icon) ("nav-item", None)
};
PrepareMarkup::With(html! {
li class=(class) aria-current=[aria] {
a class="nav-link" href=(item_path) title=[description] {
//(left_icon)
(label.escaped(cx.langid()))
//(right_icon)
}
} }
} })
}), }
ItemType::LinkBlank(label, path) => PrepareMarkup::With(html! { ItemType::LinkBlank(label, path) => {
li class="nav-item" { let item_path = path(cx);
a class="nav-link" href=(path(cx)) title=[description] target="_blank" { let (class, aria) = if item_path == current_path {
//(left_icon) ("nav-item active", Some("page"))
(label.escaped(cx.langid())) } else {
//(right_icon) ("nav-item", None)
};
PrepareMarkup::With(html! {
li class=(class) aria-current=[aria] {
a class="nav-link" href=(item_path) title=[description] target="_blank" {
//(left_icon)
(label.escaped(cx.langid()))
//(right_icon)
}
} }
} })
}), }
} }
} }
} }

View file

@ -1,8 +1,7 @@
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::bs::{ use crate::bs::BreakPoint;
BreakPoint, OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility, use crate::bs::{OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility};
};
use crate::LOCALES_BOOTSIER; use crate::LOCALES_BOOTSIER;
#[rustfmt::skip] #[rustfmt::skip]
@ -32,7 +31,7 @@ impl ComponentTrait for Offcanvas {
self.alter_classes( self.alter_classes(
ClassesOp::Prepend, ClassesOp::Prepend,
[ [
self.breakpoint().breakpoint_class("offcanvas"), self.breakpoint().to_class("offcanvas"),
self.placement().to_string(), self.placement().to_string(),
self.visibility().to_string(), self.visibility().to_string(),
] ]