✨ (base): Añade componentes Nav y Navbar
Dropdown incorpora tamaño y estilo de botón configurables (`button::Size`/`Style`), reutilizados también por Navbar. El tema Basic sirve `basic.min.css` y añade el JS de inicialización de Navbar.
This commit is contained in:
parent
da959183f6
commit
35ae5bb6a8
22 changed files with 1695 additions and 280 deletions
|
|
@ -31,6 +31,12 @@ pub struct Dropdown {
|
|||
title: Lc,
|
||||
/// Devuelve si el botón se desdobla (*split*) en botón de acción más *toggle*.
|
||||
button_split: bool,
|
||||
/// Devuelve el tamaño visual del botón.
|
||||
#[getters(copy)]
|
||||
button_size: button::Size,
|
||||
/// Devuelve el estilo visual del botón.
|
||||
#[getters(copy)]
|
||||
button_style: button::Style,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
|
@ -65,15 +71,44 @@ impl Component for Dropdown {
|
|||
});
|
||||
}
|
||||
|
||||
// Sin tamaño ni estilo (caso más común), evita construir dinámicamente las clases.
|
||||
let (button_classes, toggle_classes): (CowStr, CowStr) =
|
||||
if matches!(self.button_size(), button::Size::None)
|
||||
&& matches!(self.button_style(), button::Style::None)
|
||||
{
|
||||
(
|
||||
CowStr::from("dropdown-button"),
|
||||
CowStr::from("dropdown-toggle"),
|
||||
)
|
||||
} else {
|
||||
use button::{Size, Style};
|
||||
|
||||
let size_class = match self.button_size() {
|
||||
Size::None => "",
|
||||
Size::Small => " button-sm",
|
||||
Size::Large => " button-lg",
|
||||
};
|
||||
let style_class = match self.button_style() {
|
||||
Style::None => String::new(),
|
||||
Style::Solid(intent) => util::join!(" button-", intent.color(cx)),
|
||||
Style::Outline(intent) => util::join!(" button-outline-", intent.color(cx)),
|
||||
Style::Link => " button-link".to_string(),
|
||||
};
|
||||
(
|
||||
util::join!("dropdown-button", size_class, &style_class).into(),
|
||||
util::join!("dropdown-toggle", size_class, &style_class).into(),
|
||||
)
|
||||
};
|
||||
|
||||
let toggle_label = Lc::l("dropdown_toggle").using(cx);
|
||||
|
||||
Ok(html! {
|
||||
div (self.props()) {
|
||||
@if *self.button_split() {
|
||||
button type="button" class="dropdown-button" { (&title) }
|
||||
button type="button" class=(&button_classes) { (&title) }
|
||||
button
|
||||
type="button"
|
||||
class="dropdown-toggle"
|
||||
class=(&toggle_classes)
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
{
|
||||
|
|
@ -82,7 +117,7 @@ impl Component for Dropdown {
|
|||
} @else {
|
||||
button
|
||||
type="button"
|
||||
class="dropdown-toggle"
|
||||
class=(&toggle_classes)
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
{
|
||||
|
|
@ -126,6 +161,20 @@ impl Dropdown {
|
|||
self
|
||||
}
|
||||
|
||||
/// Establece el tamaño visual del botón (usa [`button::Size::None`] para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_button_size(mut self, size: button::Size) -> Self {
|
||||
self.button_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el estilo visual del botón (usa [`button::Style::None`] para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_button_style(mut self, style: button::Style) -> Self {
|
||||
self.button_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
|
||||
/// [`TypedOp`].
|
||||
///
|
||||
|
|
|
|||
10
src/base/component/nav.rs
Normal file
10
src/base/component/nav.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//! Definiciones para crear menús de navegación planos ([`Nav`]) y sus elementos ([`Item`]).
|
||||
|
||||
mod props;
|
||||
pub use props::Layout;
|
||||
|
||||
mod component;
|
||||
pub use component::Nav;
|
||||
|
||||
mod item;
|
||||
pub use item::{Item, ItemKind};
|
||||
113
src/base/component/nav/component.rs
Normal file
113
src/base/component/nav/component.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente para crear un **menú de navegación plano**.
|
||||
///
|
||||
/// Presenta una lista de elementos [`nav::Item`](super::Item), donde alguno puede desplegar un
|
||||
/// [`Dropdown`](super::super::Dropdown) embebido.
|
||||
///
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let nav = nav::Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// .with_item(nav::Item::link_blank(Lc::n("External"), "https://docs.rs"))
|
||||
/// .with_item(nav::Item::dropdown(
|
||||
/// dropdown::Dropdown::new()
|
||||
/// .with_title(Lc::n("Options"))
|
||||
/// .with_item(dropdown::Item::link(Lc::n("Action"), "/action"))
|
||||
/// .with_item(dropdown::Item::link(Lc::n("Another"), "/another")),
|
||||
/// ))
|
||||
/// .with_item(nav::Item::link_disabled(Lc::n("Disabled"), "#"));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Nav {
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||
props: Props,
|
||||
/// Devuelve la distribución y orientación seleccionada.
|
||||
nav_layout: nav::Layout,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Nav {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_prop(PropsOp::prepend_classes(match self.nav_layout() {
|
||||
nav::Layout::Default => "nav",
|
||||
nav::Layout::Start => "nav nav-start",
|
||||
nav::Layout::Center => "nav nav-center",
|
||||
nav::Layout::End => "nav nav-end",
|
||||
nav::Layout::Vertical => "nav nav-vertical",
|
||||
nav::Layout::Fill => "nav nav-fill",
|
||||
nav::Layout::Justified => "nav nav-justified",
|
||||
}));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let items = self.items().render(cx).await;
|
||||
if items.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
Ok(html! {
|
||||
ul (self.props()) {
|
||||
(items)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Nav {
|
||||
// **< Nav BUILDER >****************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Selecciona la distribución y orientación del menú.
|
||||
#[builder_fn]
|
||||
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
|
||||
self.nav_layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una
|
||||
/// operación [`TypedOp`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// nav.with_item(nav::Item::link("Inicio", "/"));
|
||||
/// nav.with_item(TypedOp::AddMany(vec![
|
||||
/// nav::Item::link(...),
|
||||
/// nav::Item::link_disabled(...),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, op: impl Into<TypedOp<nav::Item>>) -> Self {
|
||||
self.items.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
291
src/base/component/nav/item.rs
Normal file
291
src/base/component/nav/item.rs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
// **< ItemKind >***********************************************************************************
|
||||
|
||||
/// Tipos de [`nav::Item`](super::Item) disponibles en un menú [`Nav`](super::Nav).
|
||||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum ItemKind {
|
||||
/// Elemento vacío, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Etiqueta sin comportamiento interactivo.
|
||||
Label(Lc),
|
||||
/// Elemento de navegación basado en una [`RoutePath`] dinámica resuelta por una [`Route`].
|
||||
/// Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente deshabilitado.
|
||||
Link {
|
||||
label: Lc,
|
||||
route: Route,
|
||||
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(Embed<Html>),
|
||||
/// Elemento que despliega un menú [`Dropdown`](super::super::Dropdown).
|
||||
Dropdown(Embed<Dropdown>),
|
||||
}
|
||||
|
||||
impl ItemKind {
|
||||
const ITEM: &str = "nav-item";
|
||||
const DROPDOWN: &str = "nav-item dropdown";
|
||||
|
||||
/// Devuelve las clases base asociadas al tipo de elemento.
|
||||
#[inline]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Void => "",
|
||||
Self::Dropdown(_) => Self::DROPDOWN,
|
||||
_ => Self::ITEM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Representa un **elemento individual** de un menú [`Nav`](super::Nav).
|
||||
///
|
||||
/// Cada instancia de [`nav::Item`](super::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, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||
props: Props,
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
item_kind: ItemKind,
|
||||
/// Devuelve el valor de `active` forzado explícitamente, si lo hay.
|
||||
///
|
||||
/// Sin forzar (`None`), un [`ItemKind::Link`] se marca activo cuando su ruta coincide
|
||||
/// exactamente con la del *request* actual (ver [`prepare()`](Component::prepare)). Forzarlo
|
||||
/// permite otros criterios, como marcar activa una sección mientras la ruta actual sea
|
||||
/// cualquiera de sus subrutas.
|
||||
active_override: Option<bool>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_prop(PropsOp::prepend_classes(self.item_kind().as_str()));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.item_kind() {
|
||||
ItemKind::Void => html! {},
|
||||
|
||||
ItemKind::Label(label) => html! {
|
||||
li (self.props()) {
|
||||
span class="nav-link disabled" aria-disabled="true" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
route,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let enabled = !*disabled;
|
||||
|
||||
// Deshabilitado, `href` no la usa: evita resolver la ruta para nada.
|
||||
let route_link = enabled.then(|| route.resolve(cx));
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = self
|
||||
.active_override()
|
||||
.copied()
|
||||
.unwrap_or(enabled && route_link.as_ref().map(RoutePath::path) == current_path);
|
||||
|
||||
let active_class = if is_current { " active" } else { "" };
|
||||
let disabled_class = if *disabled { " disabled" } else { "" };
|
||||
let classes = util::join!("nav-link", active_class, disabled_class);
|
||||
|
||||
let target = (enabled && *blank).then_some("_blank");
|
||||
let rel = (enabled && *blank).then_some("noopener noreferrer");
|
||||
let aria_current = (enabled && is_current).then_some("page");
|
||||
let aria_disabled = (*disabled).then_some("true");
|
||||
|
||||
html! {
|
||||
li (self.props()) {
|
||||
a
|
||||
class=(classes)
|
||||
href=[route_link]
|
||||
target=[target]
|
||||
rel=[rel]
|
||||
aria-current=[aria_current]
|
||||
aria-disabled=[aria_disabled]
|
||||
{
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Html(html) => html! {
|
||||
li (self.props()) {
|
||||
(html.render(cx).await)
|
||||
}
|
||||
},
|
||||
|
||||
ItemKind::Dropdown(menu) => {
|
||||
if let Some(dd) = menu.get() {
|
||||
let items = dd.items().render(cx).await;
|
||||
if items.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
let title = dd.title().using(cx);
|
||||
let title = if title.is_empty() {
|
||||
Lc::l("dropdown_default_title").using(cx)
|
||||
} else {
|
||||
title
|
||||
};
|
||||
html! {
|
||||
li (self.props()) {
|
||||
a
|
||||
class="nav-link dropdown-toggle"
|
||||
href="#"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
{
|
||||
(title)
|
||||
}
|
||||
ul class="dropdown-menu" {
|
||||
(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: Lc) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
///
|
||||
/// La ruta se obtiene invocando [`Route::resolve()`], 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: Lc, route: impl Into<Route>) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route: route.into(),
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: Lc, route: impl Into<Route>) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route: route.into(),
|
||||
blank: false,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||
pub fn link_blank(label: Lc, route: impl Into<Route>) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route: route.into(),
|
||||
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: Lc, route: impl Into<Route>) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
route: route.into(),
|
||||
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 {
|
||||
Self {
|
||||
item_kind: ItemKind::Html(Embed::with(html)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un elemento de navegación que contiene un menú desplegable
|
||||
/// [`Dropdown`](super::super::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`](super::super::Dropdown); el resto de propiedades no
|
||||
/// afectarán a su representación en [`Nav`](super::Nav).
|
||||
pub fn dropdown(menu: Dropdown) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Dropdown(Embed::with(menu)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Fuerza si un [`ItemKind::Link`] se marca activo, o `None` para volver a la detección
|
||||
/// automática por coincidencia exacta de ruta.
|
||||
#[builder_fn]
|
||||
pub fn with_active(mut self, active: impl Into<Option<bool>>) -> Self {
|
||||
self.active_override = active.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
26
src/base/component/nav/props.rs
Normal file
26
src/base/component/nav/props.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Distribución y orientación de un menú [`Nav`](super::Nav).
|
||||
///
|
||||
/// Las variantes son puramente semánticas: cada tema decide en su propio `setup()` qué clases CSS
|
||||
/// les corresponden, según su propio vocabulario de estilos.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Layout {
|
||||
/// Comportamiento por defecto, ancho definido por el contenido y sin alineación forzada.
|
||||
#[default]
|
||||
Default,
|
||||
/// Alinea los elementos al inicio de la fila.
|
||||
Start,
|
||||
/// Centra horizontalmente los elementos.
|
||||
Center,
|
||||
/// Alinea los elementos al final de la fila.
|
||||
End,
|
||||
/// Apila los elementos en columna.
|
||||
Vertical,
|
||||
/// Los elementos se expanden para rellenar la fila.
|
||||
Fill,
|
||||
/// Todos los elementos ocupan el mismo ancho rellenando la fila.
|
||||
Justified,
|
||||
}
|
||||
10
src/base/component/navbar.rs
Normal file
10
src/base/component/navbar.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//! Definiciones para crear barras de navegación ([`Navbar`]) y sus elementos ([`Item`]).
|
||||
|
||||
mod props;
|
||||
pub use props::Layout;
|
||||
|
||||
mod component;
|
||||
pub use component::Navbar;
|
||||
|
||||
mod item;
|
||||
pub use item::Item;
|
||||
238
src/base/component/navbar/component.rs
Normal file
238
src/base/component/navbar/component.rs
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Componente para crear una **barra de navegación**.
|
||||
///
|
||||
/// Permite mostrar enlaces, menús desplegables ([`nav::Item::dropdown()`]) y una marca de
|
||||
/// identidad, en distintas disposiciones controladas por [`navbar::Layout`].
|
||||
///
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// Barra **simple**, sólo con un menú horizontal:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pagetop::prelude::*;
|
||||
///
|
||||
/// let navbar = Navbar::simple()
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// .with_item(nav::Item::link(Lc::n("About"), "/about"))
|
||||
/// .with_item(nav::Item::link(Lc::n("Contact"), "/contact")),
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra **colapsable**, con botón de despliegue:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let navbar = Navbar::simple_toggle()
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// .with_item(nav::Item::link_blank(Lc::n("Doc"), "https://docs.rs"))
|
||||
/// .with_item(nav::Item::link(Lc::n("Support"), "/support")),
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra con **marca de identidad** y menú, con menús desplegables:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let brand = Brand::new()
|
||||
/// .with_title(Lc::n("PageTop"))
|
||||
/// .with_route(Route::from("/"));
|
||||
///
|
||||
/// let navbar = Navbar::brand_left(brand)
|
||||
/// .with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Home"), "/"))
|
||||
/// .with_item(nav::Item::dropdown(
|
||||
/// Dropdown::new()
|
||||
/// .with_title(Lc::n("Tools"))
|
||||
/// .with_item(dropdown::Item::link(Lc::n("Generator"), "/tools/gen"))
|
||||
/// .with_item(dropdown::Item::link(Lc::n("Reports"), "/tools/reports")),
|
||||
/// ))
|
||||
/// .with_item(nav::Item::link_disabled(Lc::n("Disabled"), "#")),
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// Barra con **botón de despliegue** y **marca de identidad**, en ese orden:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use pagetop::prelude::*;
|
||||
/// let brand = Brand::new()
|
||||
/// .with_title(Lc::n("Intranet"))
|
||||
/// .with_route(Route::from("/"));
|
||||
///
|
||||
/// let navbar = Navbar::brand_right(brand).with_item(navbar::Item::nav(
|
||||
/// Nav::new()
|
||||
/// .with_item(nav::Item::link(Lc::n("Dashboard"), "/dashboard"))
|
||||
/// .with_item(nav::Item::link(Lc::n("Users"), "/users")),
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// [`nav::Item::dropdown()`]: super::super::nav::Item::dropdown
|
||||
/// [`navbar::Layout`]: super::Layout
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Navbar {
|
||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||
props: Props,
|
||||
/// Devuelve la disposición configurada para la barra de navegación.
|
||||
layout: navbar::Layout,
|
||||
/// Devuelve la lista de contenidos.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Navbar {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.props.get_id()
|
||||
}
|
||||
|
||||
fn setup(&mut self, cx: &Context) {
|
||||
// Asegura que la barra de navegación tiene un identificador único: lo necesita el botón de
|
||||
// despliegue para referenciar el contenido colapsable con `aria-controls`.
|
||||
self.alter_prop(PropsOp::ensure_id(cx.build_id::<Self>(1)));
|
||||
self.alter_prop(PropsOp::prepend_classes("navbar"));
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
// Botón de despliegue para el contenido colapsable de la barra.
|
||||
fn button(cx: &mut Context, id_content: &str) -> Markup {
|
||||
html! {
|
||||
button
|
||||
type="button"
|
||||
class="navbar-toggle"
|
||||
aria-expanded="false"
|
||||
aria-controls=(id_content)
|
||||
aria-label=[Lc::l("navbar_toggle").lookup(cx)]
|
||||
{
|
||||
span class="navbar-toggle-icon" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
||||
let items = self.items().render(cx).await;
|
||||
if items.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// `setup()` garantiza que habrá un `id` antes de renderizar.
|
||||
let id = self.id().unwrap();
|
||||
let id_content = util::join!(id, "-content");
|
||||
|
||||
Ok(html! {
|
||||
nav (self.props()) {
|
||||
@match self.layout() {
|
||||
// Barra más sencilla: sólo contenido, siempre visible.
|
||||
navbar::Layout::Simple => {
|
||||
div class="navbar-content" { (items) }
|
||||
},
|
||||
|
||||
// Barra sencilla que se puede contraer/expandir.
|
||||
navbar::Layout::SimpleToggle => {
|
||||
(button(cx, &id_content))
|
||||
div id=(&id_content) class="navbar-content" { (items) }
|
||||
},
|
||||
|
||||
// Barra con marca, siempre visible, sin botón.
|
||||
navbar::Layout::SimpleBrandLeft(brand) => {
|
||||
(brand.render(cx).await)
|
||||
div class="navbar-content" { (items) }
|
||||
},
|
||||
|
||||
// Barra con marca y botón, en ese orden.
|
||||
navbar::Layout::BrandLeft(brand) => {
|
||||
(brand.render(cx).await)
|
||||
(button(cx, &id_content))
|
||||
div id=(&id_content) class="navbar-content" { (items) }
|
||||
},
|
||||
|
||||
// Barra con botón y marca, en ese orden.
|
||||
navbar::Layout::BrandRight(brand) => {
|
||||
(button(cx, &id_content))
|
||||
div id=(&id_content) class="navbar-content" { (items) }
|
||||
(brand.render(cx).await)
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Navbar {
|
||||
/// Crea una barra de navegación **simple**, sin marca y sin botón.
|
||||
pub fn simple() -> Self {
|
||||
Self::default().with_layout(navbar::Layout::Simple)
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación **simple pero colapsable**, con botón de despliegue.
|
||||
pub fn simple_toggle() -> Self {
|
||||
Self::default().with_layout(navbar::Layout::SimpleToggle)
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación **con marca de identidad**, siempre visible, sin botón.
|
||||
pub fn simple_brand_left(brand: Brand) -> Self {
|
||||
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **marca de identidad** y **botón de despliegue**, en ese
|
||||
/// orden.
|
||||
pub fn brand_left(brand: Brand) -> Self {
|
||||
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **botón de despliegue** y **marca de identidad**, en ese
|
||||
/// orden.
|
||||
pub fn brand_right(brand: Brand) -> Self {
|
||||
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
|
||||
}
|
||||
|
||||
// **< Navbar BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
||||
self.props.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica identificador, clases CSS, atributos HTML o valores extra del componente.
|
||||
#[builder_fn]
|
||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||
self.props.alter_prop(op);
|
||||
self
|
||||
}
|
||||
|
||||
/// Define el tipo de disposición que tendrá la barra de navegación.
|
||||
#[builder_fn]
|
||||
pub fn with_layout(mut self, layout: navbar::Layout) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
||||
/// barra con una operación [`TypedOp`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// navbar.with_item(navbar::Item::nav(...));
|
||||
/// navbar.with_item(TypedOp::AddMany(vec![
|
||||
/// navbar::Item::nav(...),
|
||||
/// navbar::Item::text(...),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, op: impl Into<TypedOp<navbar::Item>>) -> Self {
|
||||
self.items.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
84
src/base/component/navbar/item.rs
Normal file
84
src/base/component/navbar/item.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Elementos que puede contener una barra de navegación [`Navbar`](super::Navbar).
|
||||
///
|
||||
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
|
||||
/// contenido** de la barra (la parte colapsable), por lo que son independientes de la marca o del
|
||||
/// botón que ya pueda definir el propio [`navbar::Layout`](super::Layout).
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum Item {
|
||||
/// Sin contenido, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Marca de identidad mostrada dentro del contenido de la barra de navegación.
|
||||
///
|
||||
/// Útil cuando el [`navbar::Layout`](super::Layout) no incluye marca, y se quiere incluir
|
||||
/// dentro del área colapsable. Si el *layout* ya muestra una marca, esta variante no la
|
||||
/// sustituye, sólo añade otra dentro del bloque de contenidos.
|
||||
Brand(Embed<Brand>),
|
||||
/// Representa un menú de navegación [`Nav`].
|
||||
Nav(Embed<Nav>),
|
||||
/// Representa un *texto localizado* libre.
|
||||
Text(Lc),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
match self {
|
||||
Self::Void => None,
|
||||
Self::Brand(brand) => brand.id(),
|
||||
Self::Nav(nav) => nav.id(),
|
||||
Self::Text(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if let Self::Nav(nav) = self
|
||||
&& let Some(nav) = nav.get_mut()
|
||||
{
|
||||
// Se añade aquí, antes de que `nav.render(cx)` (en `prepare()`) dispare el ciclo de
|
||||
// vida normal del `Nav` embebido y su propio `setup()` prepend las clases de `nav`/
|
||||
// `nav::Layout`.
|
||||
nav.alter_prop(PropsOp::prepend_classes("navbar-nav"));
|
||||
}
|
||||
}
|
||||
|
||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self {
|
||||
Self::Void => html! {},
|
||||
Self::Brand(brand) => html! { (brand.render(cx).await) },
|
||||
Self::Nav(nav) => html! { (nav.render(cx).await) },
|
||||
Self::Text(text) => html! {
|
||||
span class="navbar-text" {
|
||||
(text.using(cx))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo [`navbar::Brand`](super::super::Brand) para añadir en el contenido
|
||||
/// de [`Navbar`](super::Navbar).
|
||||
///
|
||||
/// Pensado para barras colapsables donde se quiere que la marca aparezca en la zona
|
||||
/// desplegable.
|
||||
pub fn brand(brand: Brand) -> Self {
|
||||
Self::Brand(Embed::with(brand))
|
||||
}
|
||||
|
||||
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`](super::Navbar).
|
||||
pub fn nav(item: Nav) -> Self {
|
||||
Self::Nav(Embed::with(item))
|
||||
}
|
||||
|
||||
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
||||
pub fn text(item: Lc) -> Self {
|
||||
Self::Text(item)
|
||||
}
|
||||
}
|
||||
32
src/base/component/navbar/props.rs
Normal file
32
src/base/component/navbar/props.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Representa las distintas formas de presentación de una barra de navegación
|
||||
/// [`Navbar`](super::Navbar).
|
||||
///
|
||||
/// Sólo recoge las combinaciones de marca y botón de despliegue independientes de cualquier
|
||||
/// framework CSS. Un tema puede definir su propia variante de disposición (posiciones fijas,
|
||||
/// contenido en un panel lateral...) con su propio tipo, sin depender de éste.
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum Layout {
|
||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
#[default]
|
||||
Simple,
|
||||
|
||||
/// Barra simple, con botón de despliegue y sin marca de identidad.
|
||||
SimpleToggle,
|
||||
|
||||
/// Barra simple, con marca de identidad y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
SimpleBrandLeft(Embed<Brand>),
|
||||
|
||||
/// Barra con marca de identidad y botón de despliegue, en ese orden.
|
||||
BrandLeft(Embed<Brand>),
|
||||
|
||||
/// Barra con botón de despliegue y marca de identidad, en ese orden.
|
||||
BrandRight(Embed<Brand>),
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue