WIP: Añade componente para la gestión de menús #8
8 changed files with 654 additions and 72 deletions
|
|
@ -1,3 +1,6 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Toggle Dropdown
|
||||
|
||||
# Offcanvas
|
||||
close = Close
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Mostrar/ocultar menú
|
||||
|
||||
# Offcanvas
|
||||
close = Cerrar
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
//! Coleción de elementos auxiliares de Bootstrap para Bootsier.
|
||||
//! Colección de elementos auxiliares de Bootstrap para Bootsier.
|
||||
|
||||
mod breakpoint;
|
||||
pub use breakpoint::BreakPoint;
|
||||
|
||||
mod color;
|
||||
pub use color::Color;
|
||||
pub use color::{BgColor, BorderColor, TextColor};
|
||||
pub use color::{BgColor, BorderColor, ButtonColor, TextColor};
|
||||
|
||||
mod opacity;
|
||||
pub use opacity::Opacity;
|
||||
|
|
@ -16,3 +16,6 @@ pub use border::{Border, BorderSize};
|
|||
|
||||
mod rounded;
|
||||
pub use rounded::{Rounded, RoundedRadius};
|
||||
|
||||
mod size;
|
||||
pub use size::ButtonSize;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,35 @@ impl fmt::Display for BorderColor {
|
|||
}
|
||||
}
|
||||
|
||||
// **< ButtonColor >********************************************************************************
|
||||
|
||||
/// Variantes de color (`btn-*`) para **botones**.
|
||||
///
|
||||
/// - `Default` no añade clase (devuelve `""` para facilitar la composición de clases).
|
||||
/// - `Background(Color)` genera `btn-{color}` (botón relleno).
|
||||
/// - `Outline(Color)` genera `btn-outline-{color}` (contorno: texto y borde, fondo transparente).
|
||||
/// - `Link` aplica estilo de enlace (`btn-link`), sin caja ni fondo, heredando el color de texto.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum ButtonColor {
|
||||
#[default]
|
||||
Default,
|
||||
Background(Color),
|
||||
Outline(Color),
|
||||
Link,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl fmt::Display for ButtonColor {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Default => Ok(()),
|
||||
Self::Background(c) => write!(f, "btn-{c}"),
|
||||
Self::Outline(c) => write!(f, "btn-outline-{c}"),
|
||||
Self::Link => f.write_str("btn-link"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< TextColor >**********************************************************************************
|
||||
|
||||
/// Colores de texto y fondos de texto (`text-*`).
|
||||
|
|
|
|||
31
extensions/pagetop-bootsier/src/theme/aux/size.rs
Normal file
31
extensions/pagetop-bootsier/src/theme/aux/size.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
// **< ButtonSize >*********************************************************************************
|
||||
|
||||
/// Tamaño visual de un botón.
|
||||
///
|
||||
/// Controla la escala del botón según el diseño del tema:
|
||||
///
|
||||
/// - `Default`, tamaño por defecto del tema (no añade clase).
|
||||
/// - `Small`, botón compacto.
|
||||
/// - `Large`, botón destacado/grande.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum ButtonSize {
|
||||
#[default]
|
||||
Default,
|
||||
Small,
|
||||
Large,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl fmt::Display for ButtonSize {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Default => Ok(()),
|
||||
Self::Small => f.write_str("btn-sm"),
|
||||
Self::Large => f.write_str("btn-lg"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,18 @@
|
|||
//! Definiciones para crear menús desplegables [`Dropdown`].
|
||||
//!
|
||||
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
|
||||
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad: enlaces de
|
||||
//! navegación, botones de acción, encabezados o divisores visuales.
|
||||
//!
|
||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
||||
//!
|
||||
//! Su propósito es ofrecer una base uniforme sobre la que construir menús consistentes, adaptados
|
||||
//! al contexto de cada aplicación.
|
||||
|
||||
mod component;
|
||||
pub use component::Dropdown;
|
||||
pub use component::{AutoClose, Direction, MenuAlign, MenuPosition};
|
||||
|
||||
mod item;
|
||||
pub use item::Item;
|
||||
pub use item::{Item, ItemKind};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,134 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
// **< AutoClose >**********************************************************************************
|
||||
|
||||
/// Estrategia para el cierre automático de un menú [`Dropdown`].
|
||||
///
|
||||
/// Define cuándo se cierra el menú desplegado según la interacción del usuario.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum AutoClose {
|
||||
/// Comportamiento por defecto, se cierra con clics dentro y fuera del menú, o pulsando `Esc`.
|
||||
#[default]
|
||||
Default,
|
||||
/// Sólo se cierra con clics dentro del menú.
|
||||
ClickableInside,
|
||||
/// Sólo se cierra con clics fuera del menú.
|
||||
ClickableOutside,
|
||||
/// Cierre manual, no se cierra con clics; sólo al pulsar nuevamente el botón del menú
|
||||
/// (*toggle*), o pulsando `Esc`.
|
||||
ManualClose,
|
||||
}
|
||||
|
||||
// **< Direction >**********************************************************************************
|
||||
|
||||
/// Dirección de despliegue de un menú [`Dropdown`].
|
||||
///
|
||||
/// Controla desde qué posición se muestra el menú respecto al botón.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum Direction {
|
||||
/// Comportamiento por defecto (despliega el menú hacia abajo desde la posición inicial,
|
||||
/// respetando LTR/RTL).
|
||||
#[default]
|
||||
Default,
|
||||
/// Centra horizontalmente el menú respecto al botón.
|
||||
Centered,
|
||||
/// Despliega el menú hacia arriba.
|
||||
Dropup,
|
||||
/// Despliega el menú hacia arriba y centrado.
|
||||
DropupCentered,
|
||||
/// Despliega el menú desde el lateral final, respetando LTR/RTL.
|
||||
Dropend,
|
||||
/// Despliega el menú desde el lateral inicial, respetando LTR/RTL.
|
||||
Dropstart,
|
||||
}
|
||||
|
||||
// **< MenuAlign >**********************************************************************************
|
||||
|
||||
/// Alineación horizontal del menú desplegable [`Dropdown`].
|
||||
///
|
||||
/// Permite alinear el menú al inicio o al final del botón (respetando LTR/RTL) y añadirle una
|
||||
/// alineación diferente a partir de un punto de ruptura ([`BreakPoint`]).
|
||||
#[derive(AutoDefault)]
|
||||
pub enum MenuAlign {
|
||||
/// Alineación al inicio (comportamiento por defecto).
|
||||
#[default]
|
||||
Start,
|
||||
/// Alineación al inicio a partir del punto de ruptura indicado.
|
||||
StartAt(BreakPoint),
|
||||
/// Alineación al inicio por defecto, y al final a partir de un punto de ruptura válido.
|
||||
StartAndEnd(BreakPoint),
|
||||
/// Alineación al final.
|
||||
End,
|
||||
/// Alineación al final a partir del punto de ruptura indicado.
|
||||
EndAt(BreakPoint),
|
||||
/// Alineación al final por defecto, y al inicio a partir de un punto de ruptura válido.
|
||||
EndAndStart(BreakPoint),
|
||||
}
|
||||
|
||||
// **< MenuPosition >*******************************************************************************
|
||||
|
||||
/// Posición relativa del menú desplegable [`Dropdown`].
|
||||
///
|
||||
/// Permite indicar un desplazamiento (*offset*) manual o referenciar al elemento padre para el
|
||||
/// cálculo de la posición.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum MenuPosition {
|
||||
/// Posicionamiento automático por defecto.
|
||||
#[default]
|
||||
Default,
|
||||
/// Desplazamiento manual en píxeles `(x, y)` aplicado al menú. Se admiten valores negativos.
|
||||
Offset(i8, i8),
|
||||
/// Posiciona el menú tomando como referencia el botón padre. Especialmente útil cuando
|
||||
/// [`button_split()`](crate::theme::Dropdown::button_split) es `true`.
|
||||
Parent,
|
||||
}
|
||||
|
||||
// **< Dropdown >**********************************************************************************
|
||||
|
||||
/// Componente para crear un **menú desplegable**.
|
||||
///
|
||||
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
|
||||
/// para mostrar un menú desplegable de elementos [`dropdown::Item`], que se muestra/oculta según la
|
||||
/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de
|
||||
/// apertura, alineación o política de cierre.
|
||||
///
|
||||
/// Sin título para el botón (ver [`with_button_title()`](Self::with_button_title)) se muestra
|
||||
/// únicamente la lista de elementos sin ningún botón para interactuar.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let dd = Dropdown::new()
|
||||
/// .with_button_title(L10n::n("Menu"))
|
||||
/// .with_button_color(ButtonColor::Background(Color::Secondary))
|
||||
/// .with_auto_close(dropdown::AutoClose::ClickableInside)
|
||||
/// .with_direction(dropdown::Direction::Dropend)
|
||||
/// .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/"))
|
||||
/// .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
|
||||
/// .add_item(dropdown::Item::divider())
|
||||
/// .add_item(dropdown::Item::header(L10n::n("User session")))
|
||||
/// .add_item(dropdown::Item::button(L10n::n("Sign out")));
|
||||
/// ```
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Dropdown {
|
||||
id : AttrId,
|
||||
classes: AttrClasses,
|
||||
items : Children,
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
button_title : L10n,
|
||||
button_size : ButtonSize,
|
||||
button_color : ButtonColor,
|
||||
button_split : bool,
|
||||
button_grouped: bool,
|
||||
auto_close : dropdown::AutoClose,
|
||||
direction : dropdown::Direction,
|
||||
menu_align : dropdown::MenuAlign,
|
||||
menu_position : dropdown::MenuPosition,
|
||||
items : Children,
|
||||
}
|
||||
|
||||
impl Component for Dropdown {
|
||||
|
|
@ -19,42 +140,134 @@ impl Component for Dropdown {
|
|||
self.id.get()
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "dropdown");
|
||||
let g = self.button_grouped();
|
||||
self.alter_classes(ClassesOp::Prepend, [
|
||||
if g { "btn-group" } else { "" },
|
||||
match self.direction() {
|
||||
Direction::Default if g => "",
|
||||
Direction::Default => "dropdown",
|
||||
Direction::Centered => "dropdown-center",
|
||||
Direction::Dropup => "dropup",
|
||||
Direction::DropupCentered => "dropup-center",
|
||||
Direction::Dropend => "dropend",
|
||||
Direction::Dropstart => "dropstart",
|
||||
}
|
||||
].join(" "));
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
// Si no hay elementos en el menú, no se prepara.
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
}
|
||||
|
||||
// Título opcional para el menú desplegable.
|
||||
let button_title = self.button_title().using(cx);
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
div id=[self.id()] class=[self.classes().get()] {
|
||||
button
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
{
|
||||
("Dropdown button")
|
||||
}
|
||||
ul class="dropdown-menu" {
|
||||
li {
|
||||
a class="dropdown-item" href="#" {
|
||||
("Action")
|
||||
@if !button_title.is_empty() {
|
||||
@let mut btn_classes = AttrClasses::new([
|
||||
"btn",
|
||||
&self.button_size().to_string(),
|
||||
&self.button_color().to_string(),
|
||||
].join(" "));
|
||||
@let (offset, reference) = match self.menu_position() {
|
||||
MenuPosition::Default => (None, None),
|
||||
MenuPosition::Offset(x, y) => (Some(format!("{x},{y}")), None),
|
||||
MenuPosition::Parent => (None, Some("parent")),
|
||||
};
|
||||
@let auto_close = match self.auto_close {
|
||||
AutoClose::Default => None,
|
||||
AutoClose::ClickableInside => Some("inside"),
|
||||
AutoClose::ClickableOutside => Some("outside"),
|
||||
AutoClose::ManualClose => Some("false"),
|
||||
};
|
||||
@let menu_classes = AttrClasses::new("dropdown-menu")
|
||||
.with_value(ClassesOp::Add, match self.menu_align() {
|
||||
MenuAlign::Start => String::new(),
|
||||
MenuAlign::StartAt(bp) => bp.try_class("dropdown-menu")
|
||||
.map_or(String::new(), |class| join!(class, "-start")),
|
||||
MenuAlign::StartAndEnd(bp) => bp.try_class("dropdown-menu")
|
||||
.map_or(
|
||||
"dropdown-menu-start".into(),
|
||||
|class| join!("dropdown-menu-start ", class, "-end")
|
||||
),
|
||||
MenuAlign::End => "dropdown-menu-end".into(),
|
||||
MenuAlign::EndAt(bp) => bp.try_class("dropdown-menu")
|
||||
.map_or(String::new(), |class| join!(class, "-end")),
|
||||
MenuAlign::EndAndStart(bp) => bp.try_class("dropdown-menu")
|
||||
.map_or(
|
||||
"dropdown-menu-end".into(),
|
||||
|class| join!("dropdown-menu-end ", class, "-start")
|
||||
),
|
||||
});
|
||||
|
||||
// Renderizado en modo split (dos botones) o simple (un botón).
|
||||
@if self.button_split() {
|
||||
// Botón principal (acción/etiqueta).
|
||||
@let btn = html! {
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.get()]
|
||||
{
|
||||
(button_title)
|
||||
}
|
||||
};
|
||||
// Botón *toggle* que abre/cierra el menú asociado.
|
||||
@let btn_toggle = html! {
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.alter_value(
|
||||
ClassesOp::Add, "dropdown-toggle dropdown-toggle-split"
|
||||
).get()]
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-offset=[offset]
|
||||
data-bs-reference=[reference]
|
||||
data-bs-auto-close=[auto_close]
|
||||
aria-expanded="false"
|
||||
{
|
||||
span class="visually-hidden" {
|
||||
(L10n::t("dropdown_toggle", &LOCALES_BOOTSIER).using(cx))
|
||||
}
|
||||
}
|
||||
};
|
||||
// Orden según dirección (en `dropstart` el *toggle* se sitúa antes).
|
||||
@match self.direction() {
|
||||
Direction::Dropstart => {
|
||||
(btn_toggle)
|
||||
ul class=[menu_classes.get()] { (items) }
|
||||
(btn)
|
||||
}
|
||||
_ => {
|
||||
(btn)
|
||||
(btn_toggle)
|
||||
ul class=[menu_classes.get()] { (items) }
|
||||
}
|
||||
}
|
||||
}
|
||||
li {
|
||||
a class="dropdown-item" href="#" {
|
||||
("Another action")
|
||||
}
|
||||
}
|
||||
li {
|
||||
a class="dropdown-item" href="#" {
|
||||
("Something else here")
|
||||
} @else {
|
||||
// Botón único con funcionalidad de *toggle*.
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.alter_value(
|
||||
ClassesOp::Add, "dropdown-toggle"
|
||||
).get()]
|
||||
data-bs-toggle="dropdown"
|
||||
data-bs-offset=[offset]
|
||||
data-bs-reference=[reference]
|
||||
data-bs-auto-close=[auto_close]
|
||||
aria-expanded="false"
|
||||
{
|
||||
(button_title)
|
||||
}
|
||||
ul class=[menu_classes.get()] { (items) }
|
||||
}
|
||||
} @else {
|
||||
// Sin botón: sólo el listado como menú contextual.
|
||||
ul class="dropdown-menu" { (items) }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -64,23 +277,91 @@ impl Component for Dropdown {
|
|||
impl Dropdown {
|
||||
// **< Dropdown BUILDER >***********************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del menú desplegable.
|
||||
#[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 menú desplegable.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el título del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_button_title(mut self, title: L10n) -> Self {
|
||||
self.button_title = title;
|
||||
self
|
||||
}
|
||||
|
||||
/// Ajusta el tamaño del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_button_size(mut self, size: ButtonSize) -> Self {
|
||||
self.button_size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Define el color/estilo del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_button_color(mut self, color: ButtonColor) -> Self {
|
||||
self.button_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Activa/desactiva el modo *split* (botón de acción + *toggle*).
|
||||
#[builder_fn]
|
||||
pub fn with_button_split(mut self, split: bool) -> Self {
|
||||
self.button_split = split;
|
||||
self
|
||||
}
|
||||
|
||||
/// Indica si el botón del menú está integrado en un grupo de botones.
|
||||
#[builder_fn]
|
||||
pub fn with_button_grouped(mut self, grouped: bool) -> Self {
|
||||
self.button_grouped = grouped;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la política de cierre automático del menú desplegable.
|
||||
#[builder_fn]
|
||||
pub fn with_auto_close(mut self, auto_close: dropdown::AutoClose) -> Self {
|
||||
self.auto_close = auto_close;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la dirección de despliegue del menú.
|
||||
#[builder_fn]
|
||||
pub fn with_direction(mut self, direction: dropdown::Direction) -> Self {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configura la alineación horizontal (con posible comportamiento *responsive* adicional).
|
||||
#[builder_fn]
|
||||
pub fn with_menu_align(mut self, align: dropdown::MenuAlign) -> Self {
|
||||
self.menu_align = align;
|
||||
self
|
||||
}
|
||||
|
||||
/// Configura la posición del menú.
|
||||
#[builder_fn]
|
||||
pub fn with_menu_position(mut self, position: dropdown::MenuPosition) -> Self {
|
||||
self.menu_position = position;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento hijo al menú.
|
||||
#[inline]
|
||||
pub fn add_item(mut self, item: dropdown::Item) -> Self {
|
||||
self.items.add(Child::with(item));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
|
||||
self.items.alter_typed(op);
|
||||
|
|
@ -89,10 +370,57 @@ impl Dropdown {
|
|||
|
||||
// **< Dropdown GETTERS >***********************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al menú desplegable.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el título del botón.
|
||||
pub fn button_title(&self) -> &L10n {
|
||||
&self.button_title
|
||||
}
|
||||
|
||||
/// Devuelve el tamaño configurado del botón.
|
||||
pub fn button_size(&self) -> &ButtonSize {
|
||||
&self.button_size
|
||||
}
|
||||
|
||||
/// Devuelve el color/estilo configurado del botón.
|
||||
pub fn button_color(&self) -> &ButtonColor {
|
||||
&self.button_color
|
||||
}
|
||||
|
||||
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
|
||||
pub fn button_split(&self) -> bool {
|
||||
self.button_split
|
||||
}
|
||||
|
||||
/// Devuelve si el botón del menú está integrado en un grupo de botones.
|
||||
pub fn button_grouped(&self) -> bool {
|
||||
self.button_grouped
|
||||
}
|
||||
|
||||
/// Devuelve la política de cierre automático del menú desplegado.
|
||||
pub fn auto_close(&self) -> &dropdown::AutoClose {
|
||||
&self.auto_close
|
||||
}
|
||||
|
||||
/// Devuelve la dirección de despliegue configurada.
|
||||
pub fn direction(&self) -> &dropdown::Direction {
|
||||
&self.direction
|
||||
}
|
||||
|
||||
/// Devuelve la configuración de alineación horizontal del menú desplegable.
|
||||
pub fn menu_align(&self) -> &dropdown::MenuAlign {
|
||||
&self.menu_align
|
||||
}
|
||||
|
||||
/// Devuelve la posición configurada para el menú desplegable.
|
||||
pub fn menu_position(&self) -> &dropdown::MenuPosition {
|
||||
&self.menu_position
|
||||
}
|
||||
|
||||
/// Devuelve la lista de elementos (`children`) del menú.
|
||||
pub fn items(&self) -> &Children {
|
||||
&self.items
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,53 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
// **< ItemType >***********************************************************************************
|
||||
// **< ItemKind >***********************************************************************************
|
||||
|
||||
/// Tipos de [`dropdown::Item`](crate::theme::dropdown::Item) disponibles en un menú desplegable
|
||||
/// [`Dropdown`](crate::theme::Dropdown).
|
||||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum ItemType {
|
||||
pub enum ItemKind {
|
||||
/// Elemento vacío, no produce salida.
|
||||
#[default]
|
||||
Void,
|
||||
/// Etiqueta sin comportamiento interactivo.
|
||||
Label(L10n),
|
||||
Link(L10n, FnPathByContext),
|
||||
LinkBlank(L10n, FnPathByContext),
|
||||
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
||||
/// inicialmente deshabilitado.
|
||||
Link {
|
||||
label: L10n,
|
||||
path: FnPathByContext,
|
||||
blank: bool,
|
||||
disabled: bool,
|
||||
},
|
||||
/// Acción ejecutable en la propia página, sin navegación asociada. Inicialmente puede estar
|
||||
/// deshabilitado.
|
||||
Button { label: L10n, disabled: bool },
|
||||
/// Título o encabezado que separa grupos de opciones.
|
||||
Header(L10n),
|
||||
/// Separador visual entre bloques de elementos.
|
||||
Divider,
|
||||
}
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Representa un **elemento individual** de un menú desplegable
|
||||
/// [`Dropdown`](crate::theme::Dropdown).
|
||||
///
|
||||
/// Cada instancia de [`dropdown::Item`](crate::theme::dropdown::Item) se traduce en un componente
|
||||
/// visible que puede comportarse como texto, enlace, botón, encabezado o separador, según su
|
||||
/// [`ItemKind`].
|
||||
///
|
||||
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
|
||||
/// manteniendo una interfaz común para renderizar todos los ítems del menú.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Item {
|
||||
item_type: ItemType,
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
|
|
@ -24,86 +55,227 @@ impl Component for Item {
|
|||
Item::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let description: Option<String> = None;
|
||||
match self.item_kind() {
|
||||
ItemKind::Void => PrepareMarkup::None,
|
||||
|
||||
// Obtiene la URL actual desde `cx.request`.
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
|
||||
match self.item_type() {
|
||||
ItemType::Void => PrepareMarkup::None,
|
||||
ItemType::Label(label) => PrepareMarkup::With(html! {
|
||||
li class="dropdown-item" {
|
||||
span title=[description] {
|
||||
//(left_icon)
|
||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
span class="dropdown-item-text" {
|
||||
(label.using(cx))
|
||||
//(right_icon)
|
||||
}
|
||||
}
|
||||
}),
|
||||
ItemType::Link(label, path) => {
|
||||
let item_path = path(cx);
|
||||
let (class, aria) = if current_path == Some(item_path) {
|
||||
("dropdown-item active", Some("page"))
|
||||
} else {
|
||||
("dropdown-item", None)
|
||||
};
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let path = path(cx);
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = !*disabled && current_path.map(|p| p == path).unwrap_or(false);
|
||||
|
||||
let mut classes = "dropdown-item".to_string();
|
||||
if is_current {
|
||||
classes.push_str(" active");
|
||||
}
|
||||
if *disabled {
|
||||
classes.push_str(" disabled");
|
||||
}
|
||||
|
||||
let href = (!disabled).then_some(path);
|
||||
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");
|
||||
let tabindex = disabled.then_some("-1");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
li class=(class) aria-current=[aria] {
|
||||
a class="nav-link" href=(item_path) title=[description] {
|
||||
//(left_icon)
|
||||
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]
|
||||
tabindex=[tabindex]
|
||||
{
|
||||
(label.using(cx))
|
||||
//(right_icon)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
ItemType::LinkBlank(label, path) => {
|
||||
let item_path = path(cx);
|
||||
let (class, aria) = if current_path == Some(item_path) {
|
||||
("dropdown-item active", Some("page"))
|
||||
} else {
|
||||
("dropdown-item", None)
|
||||
};
|
||||
|
||||
ItemKind::Button { label, disabled } => {
|
||||
let mut classes = "dropdown-item".to_owned();
|
||||
if *disabled {
|
||||
classes.push_str(" disabled");
|
||||
}
|
||||
|
||||
let aria_disabled = disabled.then_some("true");
|
||||
let disabled_attr = disabled.then_some("disabled");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
li class=(class) aria-current=[aria] {
|
||||
a class="nav-link" href=(item_path) title=[description] target="_blank" {
|
||||
//(left_icon)
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
button
|
||||
class=(classes)
|
||||
type="button"
|
||||
aria-disabled=[aria_disabled]
|
||||
disabled=[disabled_attr]
|
||||
{
|
||||
(label.using(cx))
|
||||
//(right_icon)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ItemKind::Header(label) => PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
h6 class="dropdown-header" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
ItemKind::Divider => PrepareMarkup::With(html! {
|
||||
li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} }
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: L10n) -> Self {
|
||||
Item {
|
||||
item_type: ItemType::Label(label),
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_type: ItemType::Link(label, path),
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
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, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_type: ItemType::LinkBlank(label, path),
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: true,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// Item GETTERS.
|
||||
/// Crea un enlace deshabilitado que se abriría en una nueva ventana.
|
||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_type(&self) -> &ItemType {
|
||||
&self.item_type
|
||||
/// Crea un botón de acción local, sin navegación asociada.
|
||||
pub fn button(label: L10n) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: false,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un botón deshabilitado.
|
||||
pub fn button_disabled(label: L10n) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: true,
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un encabezado para un grupo de elementos dentro del menú.
|
||||
pub fn header(label: L10n) -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Header(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un separador visual entre bloques de elementos.
|
||||
pub fn divider() -> Self {
|
||||
Item {
|
||||
item_kind: ItemKind::Divider,
|
||||
..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
|
||||
}
|
||||
|
||||
// **< Item GETTERS >***************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al elemento.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el tipo de elemento representado por este ítem.
|
||||
pub fn item_kind(&self) -> &ItemKind {
|
||||
&self.item_kind
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue