diff --git a/packages/pagetop-bootsier/src/bs.rs b/packages/pagetop-bootsier/src/bs.rs index 1abfc280..a85bffc4 100644 --- a/packages/pagetop-bootsier/src/bs.rs +++ b/packages/pagetop-bootsier/src/bs.rs @@ -18,7 +18,7 @@ pub use offcanvas::{ // 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. #[rustfmt::skip] @@ -37,10 +37,9 @@ pub enum 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 `false` si es `None`, `Fluid` o `FluidMax`. + /// Devuelve `true` si el valor es `SM`, `MD`, `LG`, `XL` o `XXL`. Y `false` en otro caso. pub fn is_breakpoint(&self) -> bool { !matches!( self, @@ -50,8 +49,8 @@ impl BreakPoint { /// 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 - /// proporcionado, un guion (`-`) y el texto asociado al punto de interrupción. En otro caso, se + /// 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 únicamente el prefijo. /// /// # Parámetros @@ -62,14 +61,14 @@ impl BreakPoint { /// /// ```rust#ignore /// let breakpoint = BreakPoint::MD; - /// let class = breakpoint.breakpoint_class("col"); + /// let class = breakpoint.to_class("col"); /// assert_eq!(class, "col-md".to_string()); /// /// let breakpoint = BreakPoint::Fluid; - /// let class = breakpoint.breakpoint_class("offcanvas"); + /// let class = breakpoint.to_class("offcanvas"); /// assert_eq!(class, "offcanvas".to_string()); /// ``` - pub fn breakpoint_class(&self, prefix: impl Into) -> String { + pub fn to_class(&self, prefix: impl Into) -> String { let prefix: String = prefix.into(); if self.is_breakpoint() { join_string!(prefix, "-", self.to_string()) @@ -77,6 +76,41 @@ impl BreakPoint { 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) -> Option { + 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. diff --git a/packages/pagetop-bootsier/src/bs/navbar.rs b/packages/pagetop-bootsier/src/bs/navbar.rs index d5aab896..e5454d79 100644 --- a/packages/pagetop-bootsier/src/bs/navbar.rs +++ b/packages/pagetop-bootsier/src/bs/navbar.rs @@ -1,8 +1,5 @@ mod component; -pub use component::{Navbar, NavbarType}; - -mod element; -pub use element::{Element, ElementType}; +pub use component::{Navbar, NavbarContent, NavbarToggler}; mod nav; pub use nav::Nav; diff --git a/packages/pagetop-bootsier/src/bs/navbar/component.rs b/packages/pagetop-bootsier/src/bs/navbar/component.rs index 7f2586aa..e16cf2cc 100644 --- a/packages/pagetop-bootsier/src/bs/navbar/component.rs +++ b/packages/pagetop-bootsier/src/bs/navbar/component.rs @@ -4,22 +4,33 @@ use crate::bs::navbar; use crate::bs::{BreakPoint, Offcanvas}; use crate::LOCALES_BOOTSIER; +const TOGGLE_COLLAPSE: &str = "collapse"; +const TOGGLE_OFFCANVAS: &str = "offcanvas"; + #[derive(AutoDefault)] -pub enum NavbarType { +pub enum NavbarToggler { #[default] - Default, - Basic, + Enabled, + Disabled, +} + +#[derive(AutoDefault)] +pub enum NavbarContent { + #[default] + None, + Nav(Typed), Offcanvas(Typed), + Text(L10n), } #[rustfmt::skip] #[derive(AutoDefault)] pub struct Navbar { - id : OptionId, - classes : OptionClasses, - navbar_type: NavbarType, - expand : BreakPoint, - elements : Children, + id : OptionId, + classes: OptionClasses, + expand : BreakPoint, + toggler: NavbarToggler, + content: NavbarContent, } impl ComponentTrait for Navbar { @@ -36,75 +47,58 @@ impl ComponentTrait for Navbar { ClassesOp::Prepend, [ "navbar".to_string(), - self.expand().breakpoint_class("navbar-expand"), + self.expand().try_class("navbar-expand").unwrap_or_default(), ] .join(" "), ); } 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.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::(oc.id()), - ) - } else { - (elements, join_string!(id, "-content")) - }; - let id_content_target = join_string!("#", id_content); - PrepareMarkup::With(html! { - nav id=(id) class=[self.classes().get()] { - div class="container-fluid" { - @match self.navbar_type() { - NavbarType::Default => { - button - type="button" - class="navbar-toggler" - data-bs-toggle="collapse" - data-bs-target=(id_content_target) - aria-controls=(id_content) - aria-expanded="false" - 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) - }, - } + let content = match self.content() { + NavbarContent::None => return PrepareMarkup::None, + NavbarContent::Nav(nav) => { + let id_content = join_string!(id, "-content"); + match self.toggler() { + NavbarToggler::Enabled => self.toggler_wrapper( + TOGGLE_COLLAPSE, + L10n::t("toggle", &LOCALES_BOOTSIER).using(cx.langid()), + id_content, + nav.render(cx), + ), + NavbarToggler::Disabled => nav.render(cx), } } - }) + 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 { + 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. #[fn_builder] @@ -119,12 +113,6 @@ impl Navbar { self } - #[fn_builder] - pub fn with_type(mut self, navbar_type: NavbarType) -> Self { - self.navbar_type = navbar_type; - self - } - #[fn_builder] pub fn with_expand(mut self, bp: BreakPoint) -> Self { self.expand = bp; @@ -132,8 +120,14 @@ impl Navbar { } #[fn_builder] - pub fn with_nav(mut self, op: TypedOp) -> Self { - self.elements.alter_typed(op); + pub fn with_toggler(mut self, toggler: NavbarToggler) -> Self { + self.toggler = toggler; + self + } + + #[fn_builder] + pub fn with_content(mut self, content: NavbarContent) -> Self { + self.content = content; self } @@ -143,15 +137,66 @@ impl Navbar { &self.classes } - pub fn navbar_type(&self) -> &NavbarType { - &self.navbar_type - } - pub fn expand(&self) -> &BreakPoint { &self.expand } - pub fn elements(&self) -> &Children { - &self.elements + pub fn toggler(&self) -> &NavbarToggler { + &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, + 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) + } + } + } } } diff --git a/packages/pagetop-bootsier/src/bs/navbar/element.rs b/packages/pagetop-bootsier/src/bs/navbar/element.rs deleted file mode 100644 index 0b952912..00000000 --- a/packages/pagetop-bootsier/src/bs/navbar/element.rs +++ /dev/null @@ -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 - } -} diff --git a/packages/pagetop-bootsier/src/bs/navbar/item.rs b/packages/pagetop-bootsier/src/bs/navbar/item.rs index bfee2bcc..89b42b45 100644 --- a/packages/pagetop-bootsier/src/bs/navbar/item.rs +++ b/packages/pagetop-bootsier/src/bs/navbar/item.rs @@ -27,6 +27,9 @@ impl ComponentTrait for Item { fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { let description: Option = None; + // Obtiene la URL actual desde `cx.request`. + let current_path = cx.request().path(); + match self.item_type() { ItemType::Void => PrepareMarkup::None, ItemType::Label(label) => PrepareMarkup::With(html! { @@ -38,24 +41,40 @@ impl ComponentTrait for Item { } } }), - ItemType::Link(label, path) => PrepareMarkup::With(html! { - li class="nav-item" { - a class="nav-link" href=(path(cx)) title=[description] { - //(left_icon) - (label.escaped(cx.langid())) - //(right_icon) + ItemType::Link(label, path) => { + let item_path = path(cx); + let (class, aria) = if item_path == current_path { + ("nav-item active", Some("page")) + } else { + ("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! { - li class="nav-item" { - a class="nav-link" href=(path(cx)) title=[description] target="_blank" { - //(left_icon) - (label.escaped(cx.langid())) - //(right_icon) + }) + } + ItemType::LinkBlank(label, path) => { + let item_path = path(cx); + let (class, aria) = if item_path == current_path { + ("nav-item active", Some("page")) + } else { + ("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) + } } - } - }), + }) + } } } } diff --git a/packages/pagetop-bootsier/src/bs/offcanvas/component.rs b/packages/pagetop-bootsier/src/bs/offcanvas/component.rs index 8c246688..3ce84230 100644 --- a/packages/pagetop-bootsier/src/bs/offcanvas/component.rs +++ b/packages/pagetop-bootsier/src/bs/offcanvas/component.rs @@ -1,8 +1,7 @@ use pagetop::prelude::*; -use crate::bs::{ - BreakPoint, OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility, -}; +use crate::bs::BreakPoint; +use crate::bs::{OffcanvasBackdrop, OffcanvasBodyScroll, OffcanvasPlacement, OffcanvasVisibility}; use crate::LOCALES_BOOTSIER; #[rustfmt::skip] @@ -32,7 +31,7 @@ impl ComponentTrait for Offcanvas { self.alter_classes( ClassesOp::Prepend, [ - self.breakpoint().breakpoint_class("offcanvas"), + self.breakpoint().to_class("offcanvas"), self.placement().to_string(), self.visibility().to_string(), ]