diff --git a/extensions/pagetop-bootsier/src/handlers/button.rs b/extensions/pagetop-bootsier/src/handlers/button.rs deleted file mode 100644 index 5846283d..00000000 --- a/extensions/pagetop-bootsier/src/handlers/button.rs +++ /dev/null @@ -1,7 +0,0 @@ -use pagetop::prelude::*; - -pub fn setup(button: &mut Button) { - button - .alter_prop(PropsOp::remove_classes("button")) - .alter_prop(PropsOp::prepend_classes("btn")); -} diff --git a/extensions/pagetop-bootsier/src/theme/bs/button.rs b/extensions/pagetop-bootsier/src/theme/bs/button.rs index eb705ce3..80c551db 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/button.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/button.rs @@ -1 +1,9 @@ +use pagetop::prelude::*; + pub use pagetop::base::component::{Button, ButtonAction}; + +// **< Button SETUP >******************************************************************************* + +pub(crate) fn setup(button: &mut Button) { + button.alter_prop(PropsOp::replace_classes("button", "btn")); +} diff --git a/src/html/props.rs b/src/html/props.rs index e0e87543..22d1ae47 100644 --- a/src/html/props.rs +++ b/src/html/props.rs @@ -112,6 +112,11 @@ pub enum PropsOp { /// Añade la clase o clases que no existan al principio de la lista. La operación se ignora si /// el valor contiene caracteres no ASCII. PrependClasses(CowStr), + /// Sustituye una o varias clases existentes (primer valor) por las clases indicadas (segundo + /// valor), insertando las nuevas en la posición de la primera clase sustituida encontrada. Si + /// ninguna de las clases a sustituir existe, la operación no tiene efecto. Se ignora si alguno + /// de los dos valores contiene caracteres no ASCII. + ReplaceClasses(CowStr, CowStr), /// Elimina la clase o clases indicadas de la lista. La operación se ignora si el valor contiene /// caracteres no ASCII. RemoveClasses(CowStr), @@ -151,6 +156,19 @@ impl PropsOp { Self::PrependClasses(classes.into()) } + /// Crea la variante [`ReplaceClasses`](Self::ReplaceClasses) con las clases a sustituir (`old`) + /// y las nuevas clases (`new`). + /// + /// ```rust + /// # use pagetop::prelude::*; + /// let props = Props::classes("button primary") + /// .with_prop(PropsOp::replace_classes("button", "btn")); + /// assert_eq!(props.get_classes(), Some("btn primary".to_string())); + /// ``` + pub fn replace_classes(old: impl Into, new: impl Into) -> Self { + Self::ReplaceClasses(old.into(), new.into()) + } + /// Crea la variante [`RemoveClasses`](Self::RemoveClasses) con la clase o clases indicadas. pub fn remove_classes(classes: impl Into) -> Self { Self::RemoveClasses(classes.into()) @@ -251,10 +269,11 @@ impl PropsOp { /// # use pagetop::prelude::*; /// let props = Props::default() /// .with_prop(PropsOp::add_classes("btn btn-primary")) -/// .with_prop(PropsOp::add_classes("active")); +/// .with_prop(PropsOp::add_classes("active")) +/// .with_prop(PropsOp::replace_classes("btn-primary", "btn-secondary")); /// /// let markup = html! { button (props) { "OK" } }; -/// assert_eq!(markup.into_string(), r#""#); +/// assert_eq!(markup.into_string(), r#""#); /// ``` /// /// # Valores extra @@ -354,6 +373,8 @@ impl Props { /// - [`PropsOp::ensure_id()`] - establece el identificador sólo si no hay ninguno definido. /// - [`PropsOp::add_classes()`] - añade clases al final (sin duplicados). /// - [`PropsOp::prepend_classes()`] - añade clases al principio (sin duplicados). + /// - [`PropsOp::replace_classes()`] - sustituye una o varias clases existentes por otras + /// nuevas, preservando su posición. /// - [`PropsOp::remove_classes()`] - elimina las clases indicadas. /// - [`PropsOp::set()`] - añade el atributo o reemplaza su valor. `set("id", ...)` aplica la /// misma normalización que `set_id()`. `set("class", ...)` reemplaza la lista de clases. @@ -389,6 +410,28 @@ impl Props { }; self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0); } + PropsOp::ReplaceClasses(old, new) => { + let Some(old) = util::normalize_ascii_or_empty(old.as_ref(), "Props::with_prop") + else { + return self; + }; + let Some(new) = util::normalize_ascii_or_empty(new.as_ref(), "Props::with_prop") + else { + return self; + }; + let mut pos = self.classes.len(); + let mut replaced = false; + for class in old.as_ref().split_ascii_whitespace() { + if let Some(replace_pos) = self.classes.iter().position(|c| c == class) { + self.classes.remove(replace_pos); + pos = pos.min(replace_pos); + replaced = true; + } + } + if replaced { + self.insert_classes(new.as_ref().split_ascii_whitespace(), pos); + } + } PropsOp::RemoveClasses(classes) => { let Some(normalized) = util::normalize_ascii_or_empty(classes.as_ref(), "Props::with_prop") diff --git a/tests/html_props_classes.rs b/tests/html_props_classes.rs index 68583bff..ededb8b2 100644 --- a/tests/html_props_classes.rs +++ b/tests/html_props_classes.rs @@ -74,6 +74,51 @@ async fn classes_prepend_ignores_empty_input() { assert_classes(&p, Some("a b")); } +// **< PropsOp::replace_classes >******************************************************************** + +#[pagetop::test] +async fn classes_replace_removes_targets_and_inserts_new_at_min_position() { + let p = Props::classes("a b c d").with_prop(PropsOp::replace_classes("c a", "x y")); + assert_classes(&p, Some("x y b d")); +} + +#[pagetop::test] +async fn classes_replace_when_none_found_does_nothing() { + let p = Props::classes("a b").with_prop(PropsOp::replace_classes("x y", "c d")); + assert_classes(&p, Some("a b")); +} + +#[pagetop::test] +async fn classes_replace_is_case_insensitive_on_targets_and_new_values_are_normalized() { + let p = Props::classes("btn btn-primary active") + .with_prop(PropsOp::replace_classes("BTN-PRIMARY", "Btn-Secondary")); + assert_classes(&p, Some("btn btn-secondary active")); +} + +#[pagetop::test] +async fn classes_replace_with_empty_new_removes_only() { + let p = Props::classes("a b c").with_prop(PropsOp::replace_classes("b", " ")); + assert_classes(&p, Some("a c")); +} + +#[pagetop::test] +async fn classes_replace_dedups_against_existing_items() { + let p = Props::classes("a b c").with_prop(PropsOp::replace_classes("b", "c d")); + assert_classes(&p, Some("a d c")); +} + +#[pagetop::test] +async fn classes_replace_ignores_target_whitespace_and_repetition() { + let p = Props::classes("a b c").with_prop(PropsOp::replace_classes(" b b ", "x y")); + assert_classes(&p, Some("a x y c")); +} + +#[pagetop::test] +async fn classes_replace_rejects_non_ascii_targets_is_noop() { + let p = Props::classes("a b c").with_prop(PropsOp::replace_classes("b ñ", "x")); + assert_classes(&p, Some("a b c")); +} + // **< PropsOp::set / remove ("class") >************************************************************ #[pagetop::test]