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
|
# Offcanvas
|
||||||
close = Close
|
close = Close
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Dropdown
|
||||||
|
dropdown_toggle = Mostrar/ocultar menú
|
||||||
|
|
||||||
# Offcanvas
|
# Offcanvas
|
||||||
close = Cerrar
|
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;
|
mod breakpoint;
|
||||||
pub use breakpoint::BreakPoint;
|
pub use breakpoint::BreakPoint;
|
||||||
|
|
||||||
mod color;
|
mod color;
|
||||||
pub use color::Color;
|
pub use color::Color;
|
||||||
pub use color::{BgColor, BorderColor, TextColor};
|
pub use color::{BgColor, BorderColor, ButtonColor, TextColor};
|
||||||
|
|
||||||
mod opacity;
|
mod opacity;
|
||||||
pub use opacity::Opacity;
|
pub use opacity::Opacity;
|
||||||
|
|
@ -16,3 +16,6 @@ pub use border::{Border, BorderSize};
|
||||||
|
|
||||||
mod rounded;
|
mod rounded;
|
||||||
pub use rounded::{Rounded, RoundedRadius};
|
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 >**********************************************************************************
|
// **< TextColor >**********************************************************************************
|
||||||
|
|
||||||
/// Colores de texto y fondos de texto (`text-*`).
|
/// 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;
|
mod component;
|
||||||
pub use component::Dropdown;
|
pub use component::Dropdown;
|
||||||
|
pub use component::{AutoClose, Direction, MenuAlign, MenuPosition};
|
||||||
|
|
||||||
mod item;
|
mod item;
|
||||||
pub use item::Item;
|
pub use item::{Item, ItemKind};
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,134 @@
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::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]
|
#[rustfmt::skip]
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub struct Dropdown {
|
pub struct Dropdown {
|
||||||
id : AttrId,
|
id : AttrId,
|
||||||
classes: AttrClasses,
|
classes : AttrClasses,
|
||||||
items : Children,
|
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 {
|
impl Component for Dropdown {
|
||||||
|
|
@ -19,42 +140,134 @@ impl Component for Dropdown {
|
||||||
self.id.get()
|
self.id.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
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 {
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
// Si no hay elementos en el menú, no se prepara.
|
||||||
let items = self.items().render(cx);
|
let items = self.items().render(cx);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
return PrepareMarkup::None;
|
return PrepareMarkup::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Título opcional para el menú desplegable.
|
||||||
|
let button_title = self.button_title().using(cx);
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
PrepareMarkup::With(html! {
|
||||||
div id=[self.id()] class=[self.classes().get()] {
|
div id=[self.id()] class=[self.classes().get()] {
|
||||||
button
|
@if !button_title.is_empty() {
|
||||||
type="button"
|
@let mut btn_classes = AttrClasses::new([
|
||||||
class="btn btn-secondary dropdown-toggle"
|
"btn",
|
||||||
data-bs-toggle="dropdown"
|
&self.button_size().to_string(),
|
||||||
aria-expanded="false"
|
&self.button_color().to_string(),
|
||||||
{
|
].join(" "));
|
||||||
("Dropdown button")
|
@let (offset, reference) = match self.menu_position() {
|
||||||
}
|
MenuPosition::Default => (None, None),
|
||||||
ul class="dropdown-menu" {
|
MenuPosition::Offset(x, y) => (Some(format!("{x},{y}")), None),
|
||||||
li {
|
MenuPosition::Parent => (None, Some("parent")),
|
||||||
a class="dropdown-item" href="#" {
|
};
|
||||||
("Action")
|
@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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} @else {
|
||||||
li {
|
// Botón único con funcionalidad de *toggle*.
|
||||||
a class="dropdown-item" href="#" {
|
button
|
||||||
("Another action")
|
type="button"
|
||||||
}
|
class=[btn_classes.alter_value(
|
||||||
}
|
ClassesOp::Add, "dropdown-toggle"
|
||||||
li {
|
).get()]
|
||||||
a class="dropdown-item" href="#" {
|
data-bs-toggle="dropdown"
|
||||||
("Something else here")
|
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 {
|
impl Dropdown {
|
||||||
// **< Dropdown BUILDER >***********************************************************************
|
// **< Dropdown BUILDER >***********************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único (`id`) del menú desplegable.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||||
self.id.alter_value(id);
|
self.id.alter_value(id);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de clases CSS aplicadas al menú desplegable.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||||
self.classes.alter_value(op, classes);
|
self.classes.alter_value(op, classes);
|
||||||
self
|
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 {
|
pub fn add_item(mut self, item: dropdown::Item) -> Self {
|
||||||
self.items.add(Child::with(item));
|
self.items.add(Child::with(item));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
|
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
|
||||||
self.items.alter_typed(op);
|
self.items.alter_typed(op);
|
||||||
|
|
@ -89,10 +370,57 @@ impl Dropdown {
|
||||||
|
|
||||||
// **< Dropdown GETTERS >***********************************************************************
|
// **< Dropdown GETTERS >***********************************************************************
|
||||||
|
|
||||||
|
/// Devuelve las clases CSS asociadas al menú desplegable.
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
pub fn classes(&self) -> &AttrClasses {
|
||||||
&self.classes
|
&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 {
|
pub fn items(&self) -> &Children {
|
||||||
&self.items
|
&self.items
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,53 @@
|
||||||
use pagetop::prelude::*;
|
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)]
|
#[derive(AutoDefault)]
|
||||||
pub enum ItemType {
|
pub enum ItemKind {
|
||||||
|
/// Elemento vacío, no produce salida.
|
||||||
#[default]
|
#[default]
|
||||||
Void,
|
Void,
|
||||||
|
/// Etiqueta sin comportamiento interactivo.
|
||||||
Label(L10n),
|
Label(L10n),
|
||||||
Link(L10n, FnPathByContext),
|
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
||||||
LinkBlank(L10n, FnPathByContext),
|
/// 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 >***************************************************************************************
|
// **< 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]
|
#[rustfmt::skip]
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
item_type: ItemType,
|
id : AttrId,
|
||||||
|
classes : AttrClasses,
|
||||||
|
item_kind: ItemKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for Item {
|
impl Component for Item {
|
||||||
|
|
@ -24,86 +55,227 @@ impl Component for Item {
|
||||||
Item::default()
|
Item::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
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`.
|
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||||
let current_path = cx.request().map(|request| request.path());
|
li id=[self.id()] class=[self.classes().get()] {
|
||||||
|
span class="dropdown-item-text" {
|
||||||
match self.item_type() {
|
|
||||||
ItemType::Void => PrepareMarkup::None,
|
|
||||||
ItemType::Label(label) => PrepareMarkup::With(html! {
|
|
||||||
li class="dropdown-item" {
|
|
||||||
span title=[description] {
|
|
||||||
//(left_icon)
|
|
||||||
(label.using(cx))
|
(label.using(cx))
|
||||||
//(right_icon)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
ItemType::Link(label, path) => {
|
|
||||||
let item_path = path(cx);
|
ItemKind::Link {
|
||||||
let (class, aria) = if current_path == Some(item_path) {
|
label,
|
||||||
("dropdown-item active", Some("page"))
|
path,
|
||||||
} else {
|
blank,
|
||||||
("dropdown-item", None)
|
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! {
|
PrepareMarkup::With(html! {
|
||||||
li class=(class) aria-current=[aria] {
|
li id=[self.id()] class=[self.classes().get()] {
|
||||||
a class="nav-link" href=(item_path) title=[description] {
|
a
|
||||||
//(left_icon)
|
class=(classes)
|
||||||
|
href=[href]
|
||||||
|
target=[target]
|
||||||
|
rel=[rel]
|
||||||
|
aria-current=[aria_current]
|
||||||
|
aria-disabled=[aria_disabled]
|
||||||
|
tabindex=[tabindex]
|
||||||
|
{
|
||||||
(label.using(cx))
|
(label.using(cx))
|
||||||
//(right_icon)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ItemType::LinkBlank(label, path) => {
|
|
||||||
let item_path = path(cx);
|
ItemKind::Button { label, disabled } => {
|
||||||
let (class, aria) = if current_path == Some(item_path) {
|
let mut classes = "dropdown-item".to_owned();
|
||||||
("dropdown-item active", Some("page"))
|
if *disabled {
|
||||||
} else {
|
classes.push_str(" disabled");
|
||||||
("dropdown-item", None)
|
}
|
||||||
};
|
|
||||||
|
let aria_disabled = disabled.then_some("true");
|
||||||
|
let disabled_attr = disabled.then_some("disabled");
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
PrepareMarkup::With(html! {
|
||||||
li class=(class) aria-current=[aria] {
|
li id=[self.id()] class=[self.classes().get()] {
|
||||||
a class="nav-link" href=(item_path) title=[description] target="_blank" {
|
button
|
||||||
//(left_icon)
|
class=(classes)
|
||||||
|
type="button"
|
||||||
|
aria-disabled=[aria_disabled]
|
||||||
|
disabled=[disabled_attr]
|
||||||
|
{
|
||||||
(label.using(cx))
|
(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 {
|
impl Item {
|
||||||
|
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||||
pub fn label(label: L10n) -> Self {
|
pub fn label(label: L10n) -> Self {
|
||||||
Item {
|
Item {
|
||||||
item_type: ItemType::Label(label),
|
item_kind: ItemKind::Label(label),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Crea un enlace para la navegación.
|
||||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||||
Item {
|
Item {
|
||||||
item_type: ItemType::Link(label, path),
|
item_kind: ItemKind::Link {
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
blank: false,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
..Default::default()
|
..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 {
|
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||||
Item {
|
Item {
|
||||||
item_type: ItemType::LinkBlank(label, path),
|
item_kind: ItemKind::Link {
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
blank: true,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
..Default::default()
|
..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 {
|
/// Crea un botón de acción local, sin navegación asociada.
|
||||||
&self.item_type
|
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