pagetop/extensions/pagetop-bootsier/src/theme/bs/navbar/component.rs

361 lines
14 KiB
Rust

use pagetop::prelude::*;
use crate::LOCALES_BOOTSIER;
use crate::theme::*;
const TOGGLE_COLLAPSE: &str = "collapse";
const TOGGLE_OFFCANVAS: &str = "offcanvas";
const EXTRA_LAYOUT: &str = "bootsier.navbar.layout";
const EXTRA_POSITION: &str = "bootsier.navbar.position";
const EXTRA_EXPAND: &str = "bootsier.navbar.expand";
/// Extensión de Bootsier para [`Navbar`](crate::theme::bs::Navbar).
///
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
/// botón de despliegue o dentro de un [`Offcanvas`](crate::theme::bs::Offcanvas)), controladas por
/// [`navbar::Layout`](crate::theme::bs::navbar::Layout). También puede fijarse en la parte superior
/// o inferior del documento mediante [`navbar::Position`](crate::theme::bs::navbar::Position), y
/// definir a partir de qué punto de ruptura deja de colapsar con
/// [`with_expand()`](Self::with_expand).
///
/// # Ejemplos
///
/// Barra **simple**, sólo con un menú horizontal:
///
/// ```rust,no_run
/// use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let navbar = bs::Navbar::simple()
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(Lc::n("Home"), "/"))
/// .with_item(bs::nav::Item::link(Lc::n("About"), "/about"))
/// .with_item(bs::nav::Item::link(Lc::n("Contact"), "/contact"))
/// ));
/// ```
///
/// Barra **colapsable**, con botón de despliegue y contenido en el desplegable cuando colapsa:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let navbar = bs::Navbar::simple_toggle()
/// .with_expand(BreakPoint::MD)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(Lc::n("Home"), "/"))
/// .with_item(bs::nav::Item::link_blank(Lc::n("Doc"), "https://docs.rs"))
/// .with_item(bs::nav::Item::link(Lc::n("Support"), "/support"))
/// ));
/// ```
///
/// Barra con **marca de identidad a la izquierda** y menú a la derecha, típica de una cabecera:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = Brand::new()
/// .with_title(Lc::n("PageTop"))
/// .with_route(Route::from("/"));
///
/// let navbar = bs::Navbar::brand_left(brand)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(Lc::n("Home"), "/"))
/// .with_item(bs::nav::Item::dropdown(
/// bs::Dropdown::new()
/// .with_title(Lc::n("Tools"))
/// .with_item(bs::dropdown::Item::link(
/// Lc::n("Generator"), "/tools/gen")
/// )
/// .with_item(bs::dropdown::Item::link(
/// Lc::n("Reports"), "/tools/reports")
/// )
/// ))
/// .with_item(bs::nav::Item::link_disabled(Lc::n("Disabled"), "#"))
/// ));
/// ```
///
/// Barra con **botón de despliegue a la izquierda** y **marca de identidad a la derecha**:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = Brand::new()
/// .with_title(Lc::n("Intranet"))
/// .with_route(Route::from("/"));
///
/// let navbar = bs::Navbar::brand_right(brand)
/// .with_expand(BreakPoint::LG)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_kind(bs::nav::Kind::Pills)
/// .with_item(bs::nav::Item::link(Lc::n("Dashboard"), "/dashboard"))
/// .with_item(bs::nav::Item::link(Lc::n("Users"), "/users"))
/// ));
/// ```
///
/// Barra con el **contenido en un *offcanvas***, ideal para dispositivos móviles o menús largos:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let oc = bs::Offcanvas::new()
/// .with_id("main_offcanvas")
/// .with_title(Lc::n("Main menu"))
/// .with_placement(bs::offcanvas::Placement::Start)
/// .with_backdrop(bs::offcanvas::Backdrop::Enabled);
///
/// let navbar = bs::Navbar::offcanvas(oc)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(Lc::n("Home"), "/"))
/// .with_item(bs::nav::Item::link(Lc::n("Profile"), "/profile"))
/// .with_item(bs::nav::Item::dropdown(
/// bs::Dropdown::new()
/// .with_title(Lc::n("More"))
/// .with_item(bs::dropdown::Item::link(Lc::n("Settings"), "/settings"))
/// .with_item(bs::dropdown::Item::link(Lc::n("Help"), "/help"))
/// ))
/// ));
/// ```
///
/// Barra **fija arriba**:
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::theme::*;
/// let brand = Brand::new()
/// .with_title(Lc::n("Main App"))
/// .with_route(Route::from("/"));
///
/// let navbar = bs::Navbar::brand_left(brand)
/// .with_position(bs::navbar::Position::FixedTop)
/// .with_item(bs::navbar::Item::nav(
/// bs::Nav::new()
/// .with_item(bs::nav::Item::link(Lc::n("Dashboard"), "/"))
/// .with_item(bs::nav::Item::link(Lc::n("Donors"), "/donors"))
/// .with_item(bs::nav::Item::link(Lc::n("Stock"), "/stock"))
/// ));
/// ```
#[builder_impl]
pub trait NavbarBootsier {
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
fn offcanvas(oc: bs::Offcanvas) -> Self;
/// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**.
fn offcanvas_brand_left(brand: Brand, oc: bs::Offcanvas) -> Self;
/// Crea una barra de navegación con **marca de identidad** y contenido en **offcanvas**.
fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self;
/// Define a partir de qué punto de ruptura la barra de navegación deja de colapsar.
fn with_expand(self, bp: BreakPoint) -> Self;
/// Define dónde se mostrará la barra de navegación dentro del documento.
fn with_position(self, position: bs::navbar::Position) -> Self;
}
#[builder_impl]
impl NavbarBootsier for Navbar {
fn offcanvas(oc: bs::Offcanvas) -> Self {
let mut navbar = Self::new();
navbar.alter_prop(PropsOp::set_extra(
EXTRA_LAYOUT,
bs::navbar::Layout::Offcanvas(Embed::with(oc)),
));
navbar
}
fn offcanvas_brand_left(brand: Brand, oc: bs::Offcanvas) -> Self {
let mut navbar = Self::new();
navbar.alter_prop(PropsOp::set_extra(
EXTRA_LAYOUT,
bs::navbar::Layout::OffcanvasBrandLeft(Embed::with(brand), Embed::with(oc)),
));
navbar
}
fn offcanvas_brand_right(brand: Brand, oc: bs::Offcanvas) -> Self {
let mut navbar = Self::new();
navbar.alter_prop(PropsOp::set_extra(
EXTRA_LAYOUT,
bs::navbar::Layout::OffcanvasBrandRight(Embed::with(brand), Embed::with(oc)),
));
navbar
}
fn with_expand(mut self, bp: BreakPoint) -> Self {
self.alter_prop(PropsOp::set_extra(EXTRA_EXPAND, bp));
self
}
fn with_position(mut self, position: bs::navbar::Position) -> Self {
self.alter_prop(PropsOp::set_extra(EXTRA_POSITION, position));
self
}
}
// **< Navbar SETUP >*******************************************************************************
pub(crate) fn setup(navbar: &mut Navbar) {
// Sin `with_expand()`, colapsa por debajo de 768px, igual que el tema Basic (que no tiene
// punto de ruptura configurable y siempre usa ese umbral, ver `static/css/basic.css`).
let expand = navbar.props().extra_or(EXTRA_EXPAND, BreakPoint::MD);
let position = navbar
.props()
.extra_or(EXTRA_POSITION, bs::navbar::Position::default());
let mut classes = String::new();
expand.push_to(&mut classes, "navbar-expand", "");
position.push_to(&mut classes);
if !classes.is_empty() {
navbar.alter_prop(PropsOp::add_classes(classes));
}
}
// **< Navbar RENDER >******************************************************************************
pub(crate) async fn render(navbar: &Navbar, cx: &mut Context) -> Result<Markup, ComponentError> {
// Botón de despliegue (colapso u offcanvas) para la barra.
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
let id_content_target = util::join!("#", 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=[Lc::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
{
span class="navbar-toggler-icon" {}
}
}
}
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
let items = navbar.items().render(cx).await;
if items.is_empty() {
return Ok(html! {});
}
// `Navbar::setup()` (base) garantiza que habrá un `id` antes de renderizar.
let id = navbar.id().unwrap();
// `with_layout()` (extra propio de Bootsier) tiene prioridad; si no se ha usado, se traduce el
// `navbar::Layout` de base que hayan podido fijar los constructores heredados de `Navbar`
// (`simple()`, `brand_left()`...), que Bootsier no puede sobrescribir por nombre -las funciones
// inherentes de base siempre ganan sobre las de un trait con el mismo nombre-.
let layout = navbar
.props()
.extra::<bs::navbar::Layout>(EXTRA_LAYOUT)
.cloned()
.unwrap_or_else(|_| translate_layout(navbar.layout()));
Ok(html! {
nav (navbar.props().unpack(cx)) {
div class="container-fluid" {
@match layout {
// Barra más sencilla: sólo contenido.
bs::navbar::Layout::Simple => {
(items)
},
// Barra sencilla que se puede contraer/expandir.
bs::navbar::Layout::SimpleToggle => {
@let id_content = util::join!(&id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra con marca a la izquierda, siempre visible.
bs::navbar::Layout::SimpleBrandLeft(brand) => {
(brand.render(cx).await)
(items)
},
// Barra con marca a la izquierda y botón a la derecha.
bs::navbar::Layout::BrandLeft(brand) => {
@let id_content = util::join!(&id, "-content");
(brand.render(cx).await)
(button(cx, TOGGLE_COLLAPSE, &id_content))
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra con botón a la izquierda y marca a la derecha.
bs::navbar::Layout::BrandRight(brand) => {
@let id_content = util::join!(&id, "-content");
(button(cx, TOGGLE_COLLAPSE, &id_content))
(brand.render(cx).await)
div id=(&id_content) class="collapse navbar-collapse" {
(items)
}
},
// Barra cuyo contenido se muestra en un offcanvas, sin marca.
bs::navbar::Layout::Offcanvas(offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(navbar.items())).await)
}
},
// Barra con marca a la izquierda y contenido en offcanvas.
bs::navbar::Layout::OffcanvasBrandLeft(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(brand.render(cx).await)
(button(cx, TOGGLE_OFFCANVAS, &id_content))
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(navbar.items())).await)
}
},
// Barra con contenido en offcanvas y marca a la derecha.
bs::navbar::Layout::OffcanvasBrandRight(brand, offcanvas) => {
@let id_content = offcanvas.id().unwrap_or_default();
(button(cx, TOGGLE_OFFCANVAS, &id_content))
(brand.render(cx).await)
@if let Some(oc) = offcanvas.get() {
(oc.render_offcanvas(cx, Some(navbar.items())).await)
}
},
}
}
}
})
}
// Traduce el `navbar::Layout` semántico de base (sin `Offcanvas`, sin `Position`/`expand`) a la
// variante equivalente de `bs::navbar::Layout`, para las barras construidas con los constructores
// heredados de `Navbar` (`simple()`, `simple_toggle()`, `simple_brand_left()`, `brand_left()`,
// `brand_right()`) en vez de con `with_layout()`.
fn translate_layout(layout: &navbar::Layout) -> bs::navbar::Layout {
match layout {
navbar::Layout::Simple => bs::navbar::Layout::Simple,
navbar::Layout::SimpleToggle => bs::navbar::Layout::SimpleToggle,
navbar::Layout::SimpleBrandLeft(brand) => {
bs::navbar::Layout::SimpleBrandLeft(brand.clone())
}
navbar::Layout::BrandLeft(brand) => bs::navbar::Layout::BrandLeft(brand.clone()),
navbar::Layout::BrandRight(brand) => bs::navbar::Layout::BrandRight(brand.clone()),
}
}