♻️ (bootsier): Refactoriza la gestión de clases

- Mejora la legibilidad del código.
- Simplifica las alteraciones de clases en los componentes `Container`,
  `Dropdown`, `Image`, `Nav`, `Navbar` y `Offcanvas` usando métodos
  dedicados para generar clases en función de sus propiedades.
- Mejora los enums añadiendo métodos que devuelven sus clases
  asociadas, reduciendo código repetitivo.
- Elimina el trait `JoinClasses` y su implementación, integrando la
  lógica de unión de clases directamente en los componentes.
This commit is contained in:
Manuel Cillero 2025-11-15 13:16:15 +01:00
parent 748bd81bf1
commit 2e39af0856
33 changed files with 1607 additions and 647 deletions

View file

@ -45,21 +45,11 @@ impl Component for Dropdown {
self.id.get()
}
#[rustfmt::skip]
fn setup_before_prepare(&mut self, _cx: &mut Context) {
let g = self.button_grouped();
self.alter_classes(ClassesOp::Prepend, [
if g { "btn-group" } else { "" },
match self.direction() {
dropdown::Direction::Default if g => "",
dropdown::Direction::Default => "dropdown",
dropdown::Direction::Centered => "dropdown-center",
dropdown::Direction::Dropup => "dropup",
dropdown::Direction::DropupCentered => "dropup-center",
dropdown::Direction::Dropend => "dropend",
dropdown::Direction::Dropstart => "dropstart",
}
].join_classes());
self.alter_classes(
ClassesOp::Prepend,
self.direction().class_with(self.button_grouped()),
);
}
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
@ -75,41 +65,21 @@ impl Component for Dropdown {
PrepareMarkup::With(html! {
div id=[self.id()] class=[self.classes().get()] {
@if !title.is_empty() {
@let mut btn_classes = AttrClasses::new([
"btn",
&self.button_size().to_string(),
&self.button_color().to_string(),
].join_classes());
@let (offset, reference) = match self.menu_position() {
dropdown::MenuPosition::Default => (None, None),
dropdown::MenuPosition::Offset(x, y) => (Some(format!("{x},{y}")), None),
dropdown::MenuPosition::Parent => (None, Some("parent")),
};
@let auto_close = match self.auto_close {
dropdown::AutoClose::Default => None,
dropdown::AutoClose::ClickableInside => Some("inside"),
dropdown::AutoClose::ClickableOutside => Some("outside"),
dropdown::AutoClose::ManualClose => Some("false"),
};
@let menu_classes = AttrClasses::new("dropdown-menu")
.with_value(ClassesOp::Add, match self.menu_align() {
dropdown::MenuAlign::Start => String::new(),
dropdown::MenuAlign::StartAt(bp) => bp.try_class("dropdown-menu")
.map_or(String::new(), |class| join!(class, "-start")),
dropdown::MenuAlign::StartAndEnd(bp) => bp.try_class("dropdown-menu")
.map_or(
"dropdown-menu-start".into(),
|class| join!("dropdown-menu-start ", class, "-end")
),
dropdown::MenuAlign::End => "dropdown-menu-end".into(),
dropdown::MenuAlign::EndAt(bp) => bp.try_class("dropdown-menu")
.map_or(String::new(), |class| join!(class, "-end")),
dropdown::MenuAlign::EndAndStart(bp) => bp.try_class("dropdown-menu")
.map_or(
"dropdown-menu-end".into(),
|class| join!("dropdown-menu-end ", class, "-start")
),
});
@let mut btn_classes = AttrClasses::new({
let mut classes = "btn".to_string();
self.button_size().push_class(&mut classes);
self.button_color().push_class(&mut classes);
classes
});
@let pos = self.menu_position();
@let offset = pos.data_offset();
@let reference = pos.data_reference();
@let auto_close = self.auto_close.as_str();
@let menu_classes = AttrClasses::new({
let mut classes = "dropdown-menu".to_string();
self.menu_align().push_class(&mut classes);
classes
});
// Renderizado en modo split (dos botones) o simple (un botón).
@if self.button_split() {

View file

@ -115,7 +115,7 @@ impl Component for Item {
}
ItemKind::Button { label, disabled } => {
let mut classes = "dropdown-item".to_owned();
let mut classes = "dropdown-item".to_string();
if *disabled {
classes.push_str(" disabled");
}

View file

@ -7,7 +7,7 @@ use crate::prelude::*;
/// 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)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum AutoClose {
/// Comportamiento por defecto, se cierra con clics dentro y fuera del menú, o pulsando `Esc`.
#[default]
@ -21,12 +21,26 @@ pub enum AutoClose {
ManualClose,
}
impl AutoClose {
// Devuelve el valor para `data-bs-auto-close`, o `None` si es el comportamiento por defecto.
#[rustfmt::skip]
#[inline]
pub(crate) const fn as_str(self) -> Option<&'static str> {
match self {
Self::Default => None,
Self::ClickableInside => Some("inside"),
Self::ClickableOutside => Some("outside"),
Self::ManualClose => Some("false"),
}
}
}
// **< Direction >**********************************************************************************
/// Dirección de despliegue de un menú [`Dropdown`].
///
/// Controla desde qué posición se muestra el menú respecto al botón.
#[derive(AutoDefault)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum Direction {
/// Comportamiento por defecto (despliega el menú hacia abajo desde la posición inicial,
/// respetando LTR/RTL).
@ -44,13 +58,58 @@ pub enum Direction {
Dropstart,
}
impl Direction {
// Mapea la dirección teniendo en cuenta si se agrupa con otros menús [`Dropdown`].
#[rustfmt::skip ]
#[inline]
const fn as_str(self, grouped: bool) -> &'static str {
match self {
Self::Default if grouped => "",
Self::Default => "dropdown",
Self::Centered => "dropdown-center",
Self::Dropup => "dropup",
Self::DropupCentered => "dropup-center",
Self::Dropend => "dropend",
Self::Dropstart => "dropstart",
}
}
// Añade la dirección de despliegue a la cadena de clases teniendo en cuenta si se agrupa con
// otros menús [`Dropdown`].
#[inline]
pub(crate) fn push_class(self, classes: &mut String, grouped: bool) {
if grouped {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str("btn-group");
}
let class = self.as_str(grouped);
if !class.is_empty() {
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
}
// Devuelve la clase asociada a la dirección teniendo en cuenta si se agrupa con otros menús
// [`Dropdown`], o `""` si no corresponde ninguna.
#[inline]
pub(crate) fn class_with(self, grouped: bool) -> String {
let mut classes = String::new();
self.push_class(&mut classes, grouped);
classes
}
}
// **< 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)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum MenuAlign {
/// Alineación al inicio (comportamiento por defecto).
#[default]
@ -67,13 +126,74 @@ pub enum MenuAlign {
EndAndStart(BreakPoint),
}
impl MenuAlign {
#[inline]
fn push_one(classes: &mut String, class: &str) {
if class.is_empty() {
return;
}
if !classes.is_empty() {
classes.push(' ');
}
classes.push_str(class);
}
// Añade las clases de alineación a `classes` (sin incluir la base `dropdown-menu`).
#[inline]
pub(crate) fn push_class(self, classes: &mut String) {
match self {
// Alineación por defecto (start), no añade clases extra.
Self::Start => {}
// `dropdown-menu-{bp}-start`
Self::StartAt(bp) => {
let class = bp.class_with("dropdown-menu", "start");
Self::push_one(classes, &class);
}
// `dropdown-menu-start` + `dropdown-menu-{bp}-end`
Self::StartAndEnd(bp) => {
Self::push_one(classes, "dropdown-menu-start");
let bp_class = bp.class_with("dropdown-menu", "end");
Self::push_one(classes, &bp_class);
}
// `dropdown-menu-end`
Self::End => {
Self::push_one(classes, "dropdown-menu-end");
}
// `dropdown-menu-{bp}-end`
Self::EndAt(bp) => {
let class = bp.class_with("dropdown-menu", "end");
Self::push_one(classes, &class);
}
// `dropdown-menu-end` + `dropdown-menu-{bp}-start`
Self::EndAndStart(bp) => {
Self::push_one(classes, "dropdown-menu-end");
let bp_class = bp.class_with("dropdown-menu", "start");
Self::push_one(classes, &bp_class);
}
}
}
/* Devuelve las clases de alineación sin incluir `dropdown-menu` (reservado).
#[inline]
pub(crate) fn to_class(self) -> String {
let mut classes = String::new();
self.push_class(&mut classes);
classes
} */
}
// **< 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)]
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum MenuPosition {
/// Posicionamiento automático por defecto.
#[default]
@ -84,3 +204,23 @@ pub enum MenuPosition {
/// [`button_split()`](crate::theme::Dropdown::button_split) es `true`.
Parent,
}
impl MenuPosition {
// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
#[inline]
pub(crate) fn data_offset(self) -> Option<String> {
match self {
Self::Offset(x, y) => Some(format!("{x},{y}")),
_ => None,
}
}
// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
#[inline]
pub(crate) fn data_reference(self) -> Option<&'static str> {
match self {
Self::Parent => Some("parent"),
_ => None,
}
}
}