Compare commits
2 commits
87ad78778d
...
771f61bf29
| Author | SHA1 | Date | |
|---|---|---|---|
| 771f61bf29 | |||
| 654618aa68 |
17 changed files with 804 additions and 314 deletions
|
|
@ -144,14 +144,15 @@ impl Theme for Bootsier {
|
||||||
) -> Option<Result<Markup, ComponentError>> {
|
) -> Option<Result<Markup, ComponentError>> {
|
||||||
setup_component!(component, {
|
setup_component!(component, {
|
||||||
Button => |c| theme::bs::button::setup(c),
|
Button => |c| theme::bs::button::setup(c),
|
||||||
|
Container => |c| theme::bs::container::setup(c),
|
||||||
form::input::Field => |c| theme::bs::form::input::setup(c),
|
form::input::Field => |c| theme::bs::form::input::setup(c),
|
||||||
form::select::Field => |c| theme::bs::form::select::setup(c),
|
form::select::Field => |c| theme::bs::form::select::setup(c),
|
||||||
form::Textarea => |c| theme::bs::form::textarea::setup(c),
|
form::Textarea => |c| theme::bs::form::textarea::setup(c),
|
||||||
});
|
});
|
||||||
render_component!(component, {
|
render_component!(component, {
|
||||||
form::input::Field => |c| theme::bs::form::input::render(c, cx),
|
form::input::Field => |c| theme::bs::form::input::render(c, cx),
|
||||||
form::select::Field => |c| theme::bs::form::select::render(c, cx),
|
form::select::Field => |c| theme::bs::form::select::render(c, cx),
|
||||||
form::Textarea => |c| theme::bs::form::textarea::render(c, cx),
|
form::Textarea => |c| theme::bs::form::textarea::render(c, cx),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,17 +168,17 @@ impl Theme for Bootsier {
|
||||||
.alter_assets(AssetsOp::AddStyleSheet(
|
.alter_assets(AssetsOp::AddStyleSheet(
|
||||||
StyleSheet::from("/bootsier/css/bootsier.min.css")
|
StyleSheet::from("/bootsier/css/bootsier.min.css")
|
||||||
.with_version(ADMINLTE_VERSION)
|
.with_version(ADMINLTE_VERSION)
|
||||||
.with_weight(-90),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_assets(AssetsOp::AddJavaScript(
|
.alter_assets(AssetsOp::AddJavaScript(
|
||||||
JavaScript::defer("/bootsier/js/bootsier.bundle.min.js")
|
JavaScript::defer("/bootsier/js/bootsier.bundle.min.js")
|
||||||
.with_version(BOOTSTRAP_VERSION)
|
.with_version(BOOTSTRAP_VERSION)
|
||||||
.with_weight(-90),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_assets(AssetsOp::AddJavaScript(
|
.alter_assets(AssetsOp::AddJavaScript(
|
||||||
JavaScript::defer("/bootsier/js/bootsier.extended.min.js")
|
JavaScript::defer("/bootsier/js/bootsier.extended.min.js")
|
||||||
.with_version(ADMINLTE_VERSION)
|
.with_version(ADMINLTE_VERSION)
|
||||||
.with_weight(-90),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&DefaultRegion::Footer,
|
&DefaultRegion::Footer,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,4 @@
|
||||||
//! Tipos y componentes disponibles.
|
//! Componentes proporcionados por el tema.
|
||||||
//!
|
|
||||||
//! A continuación, el apartado **Modules** incluye las definiciones necesarias para los componentes
|
|
||||||
//! que se muestran en el apartado **Structs**, mientras que en **Enums** se listan los elementos
|
|
||||||
//! auxiliares del tema utilizados en clases y componentes.
|
|
||||||
|
|
||||||
mod attrs;
|
|
||||||
pub use attrs::*;
|
|
||||||
|
|
||||||
mod classes;
|
|
||||||
pub use classes::*;
|
|
||||||
|
|
||||||
// Button.
|
// Button.
|
||||||
mod button;
|
mod button;
|
||||||
|
|
@ -18,6 +8,8 @@ pub use button::{Button, ButtonAction};
|
||||||
pub mod container;
|
pub mod container;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use container::Container;
|
pub use container::Container;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use container::ContainerBootsier;
|
||||||
|
|
||||||
// Dropdown.
|
// Dropdown.
|
||||||
pub mod dropdown;
|
pub mod dropdown;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,99 @@
|
||||||
//! Definiciones para crear contenedores de componentes ([`Container`]).
|
//! Definiciones para crear contenedores de componentes ([`Container`]).
|
||||||
//!
|
|
||||||
//! Cada contenedor envuelve contenido usando la etiqueta semántica indicada por
|
|
||||||
//! [`container::Kind`](crate::theme::bs::container::Kind).
|
|
||||||
//!
|
|
||||||
//! Con [`container::Width`](crate::theme::bs::container::Width) se puede definir el ancho y el
|
|
||||||
//! comportamiento *responsive* del contenedor. También permite aplicar utilidades de estilo para el
|
|
||||||
//! fondo, texto, borde o esquinas redondeadas.
|
|
||||||
|
|
||||||
mod props;
|
use pagetop::prelude::*;
|
||||||
pub use props::{Kind, Width};
|
|
||||||
|
|
||||||
mod component;
|
use crate::theme::*;
|
||||||
pub use component::Container;
|
|
||||||
|
pub use pagetop::base::component::container::{Container, Kind};
|
||||||
|
|
||||||
|
const EXTRA_WIDTH: &str = "bootsier.container.width";
|
||||||
|
|
||||||
|
/// Extensión de Bootsier para [`Container`].
|
||||||
|
///
|
||||||
|
/// Permite definir el comportamiento del ancho del contenedor usando el método
|
||||||
|
/// [`with_width()`](Self::with_width). También acepta clases predefinidas para:
|
||||||
|
///
|
||||||
|
/// - Modificar el color de fondo ([`Background`](crate::theme::class::Background)).
|
||||||
|
/// - Definir la apariencia del texto ([`Text`](crate::theme::class::Text)).
|
||||||
|
/// - Establecer bordes ([`Border`](crate::theme::class::Border)).
|
||||||
|
/// - Redondear las esquinas ([`Rounded`](crate::theme::class::Rounded)).
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
/// use pagetop_bootsier::theme::*;
|
||||||
|
///
|
||||||
|
/// let main = bs::Container::main()
|
||||||
|
/// .with_id("main-page")
|
||||||
|
/// .with_width(bs::container::Width::From(token::BreakPoint::LG))
|
||||||
|
/// .with_prop(PropsOp::add_classes(class::Background::with(token::Color::Light)))
|
||||||
|
/// .with_prop(PropsOp::add_classes(class::Text::with(token::Color::Dark)))
|
||||||
|
/// .with_prop(PropsOp::add_classes(class::Border::with(token::ScaleSize::One)))
|
||||||
|
/// .with_prop(PropsOp::add_classes(class::Rounded::with(token::RoundedRadius::Default)));
|
||||||
|
/// ```
|
||||||
|
pub trait ContainerBootsier {
|
||||||
|
/// Establece el comportamiento del ancho para el contenedor.
|
||||||
|
///
|
||||||
|
/// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de
|
||||||
|
/// ruptura, o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo
|
||||||
|
/// explícito. Ver [`Width`] para las variantes disponibles.
|
||||||
|
fn with_width(self, width: Width) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerBootsier for Container {
|
||||||
|
fn with_width(self, width: Width) -> Self {
|
||||||
|
self.with_prop(PropsOp::set_extra(EXTRA_WIDTH, width))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Width >**************************************************************************************
|
||||||
|
|
||||||
|
/// Define cómo se comporta el ancho de un contenedor ([`Container`]).
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Width {
|
||||||
|
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de
|
||||||
|
/// ruptura. Por debajo del menor punto de ruptura ocupa el 100% del ancho disponible.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
|
||||||
|
/// ese punto de ruptura ocupa el 100% del ancho disponible.
|
||||||
|
From(token::BreakPoint),
|
||||||
|
/// Ocupa el 100% del ancho disponible siempre.
|
||||||
|
Fluid,
|
||||||
|
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
|
||||||
|
FluidMax(UnitValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Width {
|
||||||
|
const CONTAINER: &str = "container";
|
||||||
|
|
||||||
|
/// Añade la clase asociada al ancho del contenedor a la cadena de clases.
|
||||||
|
#[inline]
|
||||||
|
pub fn push_to(self, classes: &mut String) {
|
||||||
|
match self {
|
||||||
|
Self::Default => token::BreakPoint::None.push_to(classes, Self::CONTAINER, ""),
|
||||||
|
Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""),
|
||||||
|
Self::Fluid | Self::FluidMax(_) => {
|
||||||
|
token::BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve la clase asociada al ancho del contenedor.
|
||||||
|
pub fn to_class(self) -> String {
|
||||||
|
let mut class = String::new();
|
||||||
|
self.push_to(&mut class);
|
||||||
|
class
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Container SETUP >****************************************************************************
|
||||||
|
|
||||||
|
pub(crate) fn setup(container: &mut Container) {
|
||||||
|
let width = container.props().extra_or(EXTRA_WIDTH, Width::default());
|
||||||
|
container.alter_prop(PropsOp::prepend_classes(width.to_class()));
|
||||||
|
if let Width::FluidMax(w) = width
|
||||||
|
&& w.is_measurable()
|
||||||
|
{
|
||||||
|
container.alter_prop(PropsOp::add_style("max-width", w.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,172 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::*;
|
|
||||||
|
|
||||||
/// Componente para crear un **contenedor de componentes**
|
|
||||||
/// ([`container`](crate::theme::bs::container)).
|
|
||||||
///
|
|
||||||
/// Envuelve un conjunto de componentes en un contenedor establecido que se crea aplicando uno de
|
|
||||||
/// los tipos definidos en [`container::Kind`](crate::theme::bs::container::Kind).
|
|
||||||
///
|
|
||||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// use pagetop_bootsier::theme::*;
|
|
||||||
///
|
|
||||||
/// let main = bs::Container::main()
|
|
||||||
/// .with_id("main-page")
|
|
||||||
/// .with_width(bs::container::Width::From(token::BreakPoint::LG));
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct Container {
|
|
||||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
|
||||||
props: Props,
|
|
||||||
/// Devuelve el tipo semántico del contenedor.
|
|
||||||
container_kind: bs::container::Kind,
|
|
||||||
/// Devuelve el comportamiento para el ancho del contenedor.
|
|
||||||
container_width: bs::container::Width,
|
|
||||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
|
||||||
children: Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for Container {
|
|
||||||
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.container_width().to_class()));
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
let output = self.children().render(cx).await;
|
|
||||||
if output.is_empty() {
|
|
||||||
return Ok(html! {});
|
|
||||||
}
|
|
||||||
let style = match self.container_width() {
|
|
||||||
bs::container::Width::FluidMax(w) if w.is_measurable() => {
|
|
||||||
Some(util::join!("max-width: ", w.to_string(), ";"))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
Ok(match self.container_kind() {
|
|
||||||
bs::container::Kind::Default => html! {
|
|
||||||
div (self.props()) style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bs::container::Kind::Main => html! {
|
|
||||||
main (self.props()) style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bs::container::Kind::Header => html! {
|
|
||||||
header (self.props()) style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bs::container::Kind::Footer => html! {
|
|
||||||
footer (self.props()) style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bs::container::Kind::Section => html! {
|
|
||||||
section (self.props()) style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
bs::container::Kind::Article => html! {
|
|
||||||
article (self.props()) style=[style] {
|
|
||||||
(output)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Container {
|
|
||||||
/// Crea un contenedor de tipo `Main` (`<main>`).
|
|
||||||
pub fn main() -> Self {
|
|
||||||
Self {
|
|
||||||
container_kind: bs::container::Kind::Main,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un contenedor de tipo `Header` (`<header>`).
|
|
||||||
pub fn header() -> Self {
|
|
||||||
Self {
|
|
||||||
container_kind: bs::container::Kind::Header,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un contenedor de tipo `Footer` (`<footer>`).
|
|
||||||
pub fn footer() -> Self {
|
|
||||||
Self {
|
|
||||||
container_kind: bs::container::Kind::Footer,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un contenedor de tipo `Section` (`<section>`).
|
|
||||||
pub fn section() -> Self {
|
|
||||||
Self {
|
|
||||||
container_kind: bs::container::Kind::Section,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un contenedor de tipo `Article` (`<article>`).
|
|
||||||
pub fn article() -> Self {
|
|
||||||
Self {
|
|
||||||
container_kind: bs::container::Kind::Article,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Container 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.
|
|
||||||
///
|
|
||||||
/// También acepta clases predefinidas para:
|
|
||||||
///
|
|
||||||
/// - Modificar el color de fondo ([`Background`]).
|
|
||||||
/// - Definir la apariencia del texto ([`Text`]).
|
|
||||||
/// - Establecer bordes ([`Border`]).
|
|
||||||
/// - Redondear las esquinas ([`Rounded`]).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
|
||||||
self.props.alter_prop(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece el comportamiento del ancho para el contenedor.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_width(mut self, width: bs::container::Width) -> Self {
|
|
||||||
self.container_width = width;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
|
|
||||||
/// una operación [`ChildOp`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
|
||||||
self.children.alter_child(op.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::*;
|
|
||||||
|
|
||||||
// **< Kind >***************************************************************************************
|
|
||||||
|
|
||||||
/// Tipo de contenedor ([`Container`](crate::theme::bs::Container)).
|
|
||||||
///
|
|
||||||
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
|
|
||||||
/// común a todos los contenedores.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Kind {
|
|
||||||
/// Contenedor genérico (`<div>`).
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Contenido principal de la página (`<main>`).
|
|
||||||
Main,
|
|
||||||
/// Encabezado de la página o de sección (`<header>`).
|
|
||||||
Header,
|
|
||||||
/// Pie de la página o de sección (`<footer>`).
|
|
||||||
Footer,
|
|
||||||
/// Sección de contenido (`<section>`).
|
|
||||||
Section,
|
|
||||||
/// Artículo de contenido (`<article>`).
|
|
||||||
Article,
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Width >**************************************************************************************
|
|
||||||
|
|
||||||
/// Define cómo se comporta el ancho de un contenedor ([`Container`](crate::theme::bs::Container)).
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum Width {
|
|
||||||
/// Comportamiento por defecto, aplica los anchos máximos predefinidos para cada punto de
|
|
||||||
/// ruptura. Por debajo del menor punto de ruptura ocupa el 100% del ancho disponible.
|
|
||||||
#[default]
|
|
||||||
Default,
|
|
||||||
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
|
|
||||||
/// ese punto de ruptura ocupa el 100% del ancho disponible.
|
|
||||||
From(token::BreakPoint),
|
|
||||||
/// Ocupa el 100% del ancho disponible siempre.
|
|
||||||
Fluid,
|
|
||||||
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
|
|
||||||
FluidMax(UnitValue),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Width {
|
|
||||||
const CONTAINER: &str = "container";
|
|
||||||
|
|
||||||
/// Añade la clase asociada al ancho del contenedor a la cadena de clases.
|
|
||||||
#[inline]
|
|
||||||
pub fn push_to(self, classes: &mut String) {
|
|
||||||
match self {
|
|
||||||
Self::Default => token::BreakPoint::None.push_to(classes, Self::CONTAINER, ""),
|
|
||||||
Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""),
|
|
||||||
Self::Fluid | Self::FluidMax(_) => {
|
|
||||||
token::BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Devuelve la clase asociada al ancho del contenedor.
|
|
||||||
pub fn to_class(self) -> String {
|
|
||||||
let mut class = String::new();
|
|
||||||
self.push_to(&mut class);
|
|
||||||
class
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ use pagetop::prelude::*;
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
use crate::theme::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
/// Componente para crear un **menú desplegable** ([`dropdown`](crate::theme::bs::dropdown)).
|
/// Componente para crear un **menú desplegable**.
|
||||||
///
|
///
|
||||||
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
|
/// Renderiza un botón (único o desdoblado, ver [`with_button_split()`](Self::with_button_split))
|
||||||
/// con un menú desplegable de elementos [`dropdown::Item`](crate::theme::bs::dropdown::Item), que
|
/// con un menú desplegable de elementos [`dropdown::Item`](crate::theme::bs::dropdown::Item), que
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,17 @@ pub use pagetop::base::component::form::check;
|
||||||
pub use pagetop::base::component::form::radio;
|
pub use pagetop::base::component::form::radio;
|
||||||
|
|
||||||
pub mod select;
|
pub mod select;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use select::SelectBootsier;
|
||||||
|
|
||||||
pub mod input;
|
pub mod input;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use input::InputBootsier;
|
||||||
|
|
||||||
pub mod textarea;
|
pub mod textarea;
|
||||||
pub use textarea::Textarea;
|
pub use textarea::Textarea;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use textarea::TextareaBootsier;
|
||||||
|
|
||||||
pub use pagetop::base::component::form::Range;
|
pub use pagetop::base::component::form::Range;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
/// Componente para renderizar una **imagen** ([`image`](crate::theme::bs::image)).
|
/// Componente para renderizar una **imagen**.
|
||||||
///
|
///
|
||||||
/// A una imagen se le puede:
|
/// A una imagen se le puede:
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::theme::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
/// Componente para crear un **menú** ([`nav`](crate::theme::bs::nav)).
|
/// Componente para crear un **menú**.
|
||||||
///
|
///
|
||||||
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
|
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
|
||||||
/// ([`nav::Kind`](crate::theme::bs::nav::Kind)) como *pestañas* (`Tabs`), *botones* (`Pills`) o
|
/// ([`nav::Kind`](crate::theme::bs::nav::Kind)) como *pestañas* (`Tabs`), *botones* (`Pills`) o
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use crate::theme::*;
|
||||||
const TOGGLE_COLLAPSE: &str = "collapse";
|
const TOGGLE_COLLAPSE: &str = "collapse";
|
||||||
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
||||||
|
|
||||||
/// Componente para crear una **barra de navegación** ([`navbar`](crate::theme::bs::navbar)).
|
/// Componente para crear una **barra de navegación**.
|
||||||
///
|
///
|
||||||
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
|
/// Permite mostrar enlaces, menús y una marca de identidad en distintas disposiciones (simples, con
|
||||||
/// botón de despliegue o dentro de un [`Offcanvas`](crate::theme::bs::Offcanvas)), controladas por
|
/// botón de despliegue o dentro de un [`Offcanvas`](crate::theme::bs::Offcanvas)), controladas por
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use pagetop::prelude::*;
|
||||||
use crate::LOCALES_BOOTSIER;
|
use crate::LOCALES_BOOTSIER;
|
||||||
use crate::theme::*;
|
use crate::theme::*;
|
||||||
|
|
||||||
/// Componente para crear un **panel lateral deslizante** ([`offcanvas`](crate::theme::bs::offcanvas)).
|
/// Componente para crear un **panel lateral deslizante**.
|
||||||
///
|
///
|
||||||
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
|
/// Útil para navegación, filtros, formularios o menús contextuales. Incluye las siguientes
|
||||||
/// características principales:
|
/// características principales:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ pub use block::Block;
|
||||||
mod button;
|
mod button;
|
||||||
pub use button::{Button, ButtonAction};
|
pub use button::{Button, ButtonAction};
|
||||||
|
|
||||||
|
pub mod container;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use container::Container;
|
||||||
|
|
||||||
pub mod form;
|
pub mod form;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use form::Form;
|
pub use form::Form;
|
||||||
|
|
|
||||||
145
src/base/component/container.rs
Normal file
145
src/base/component/container.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
//! Definiciones para crear contenedores de componentes ([`Container`]).
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
// **< Kind >***************************************************************************************
|
||||||
|
|
||||||
|
/// Tipo de contenedor (`Container`).
|
||||||
|
///
|
||||||
|
/// Permite aplicar la etiqueta HTML apropiada (`<main>`, `<header>`, etc.) manteniendo una API
|
||||||
|
/// común a todos los contenedores.
|
||||||
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum Kind {
|
||||||
|
/// Contenedor genérico (`<div>`).
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Contenido principal de la página (`<main>`).
|
||||||
|
Main,
|
||||||
|
/// Encabezado de la página o de sección (`<header>`).
|
||||||
|
Header,
|
||||||
|
/// Pie de la página o de sección (`<footer>`).
|
||||||
|
Footer,
|
||||||
|
/// Sección de contenido (`<section>`).
|
||||||
|
Section,
|
||||||
|
/// Artículo de contenido (`<article>`).
|
||||||
|
Article,
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Container >**********************************************************************************
|
||||||
|
|
||||||
|
/// Componente para crear un **contenedor de componentes**.
|
||||||
|
///
|
||||||
|
/// Envuelve un conjunto de componentes en un contenedor establecido que se crea aplicando uno de
|
||||||
|
/// los tipos definidos en [`Kind`].
|
||||||
|
///
|
||||||
|
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use pagetop::prelude::*;
|
||||||
|
///
|
||||||
|
/// let main = Container::main().with_id("main-page");
|
||||||
|
/// ```
|
||||||
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
pub struct Container {
|
||||||
|
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||||
|
props: Props,
|
||||||
|
/// Devuelve el tipo semántico del contenedor.
|
||||||
|
kind: Kind,
|
||||||
|
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||||
|
children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Component for Container {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.props.get_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
let output = self.children().render(cx).await;
|
||||||
|
if output.is_empty() {
|
||||||
|
return Ok(html! {});
|
||||||
|
}
|
||||||
|
Ok(match self.kind() {
|
||||||
|
Kind::Default => html! { div (self.props()) { (output) } },
|
||||||
|
Kind::Main => html! { main (self.props()) { (output) } },
|
||||||
|
Kind::Header => html! { header (self.props()) { (output) } },
|
||||||
|
Kind::Footer => html! { footer (self.props()) { (output) } },
|
||||||
|
Kind::Section => html! { section (self.props()) { (output) } },
|
||||||
|
Kind::Article => html! { article (self.props()) { (output) } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Container {
|
||||||
|
/// Crea un contenedor de tipo `Main` (`<main>`).
|
||||||
|
pub fn main() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: Kind::Main,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un contenedor de tipo `Header` (`<header>`).
|
||||||
|
pub fn header() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: Kind::Header,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un contenedor de tipo `Footer` (`<footer>`).
|
||||||
|
pub fn footer() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: Kind::Footer,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un contenedor de tipo `Section` (`<section>`).
|
||||||
|
pub fn section() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: Kind::Section,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un contenedor de tipo `Article` (`<article>`).
|
||||||
|
pub fn article() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: Kind::Article,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Container 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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
|
||||||
|
/// una operación [`ChildOp`].
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
|
self.children.alter_child(op.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Componentes y tipos para crear formularios HTML ([`Form`]).
|
//! Definiciones para crear formularios ([`Form`]).
|
||||||
|
|
||||||
mod props;
|
mod props;
|
||||||
pub use props::{Autocomplete, AutofillField, CheckboxKind, Method};
|
pub use props::{Autocomplete, AutofillField, CheckboxKind, Method};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::prelude::*;
|
||||||
|
|
||||||
use crate::base::component::form;
|
use crate::base::component::form;
|
||||||
|
|
||||||
/// Componente para crear un **formulario** HTML ([`form`]).
|
/// Componente para crear un **formulario**.
|
||||||
///
|
///
|
||||||
/// Renderiza un formulario estándar con soporte para los atributos más habituales:
|
/// Renderiza un formulario estándar con soporte para los atributos más habituales:
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::html::maud::{Escaper, Render};
|
use crate::html::maud::{Escaper, Render};
|
||||||
use crate::{AutoDefault, CowStr, builder_fn, util};
|
use crate::{AutoDefault, CowStr, builder_fn, trace, util};
|
||||||
|
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -84,18 +84,22 @@ impl std::error::Error for PropsError {}
|
||||||
/// nombre de atributo en `Set`, el valor se normaliza igual que [`SetId`](Self::SetId).
|
/// nombre de atributo en `Set`, el valor se normaliza igual que [`SetId`](Self::SetId).
|
||||||
///
|
///
|
||||||
/// Las variantes `*Classes` gestionan la lista de clases CSS. Además, `Set("class", ...)`
|
/// Las variantes `*Classes` gestionan la lista de clases CSS. Además, `Set("class", ...)`
|
||||||
/// reemplaza la lista completa y `Remove("class")` la vacía.
|
/// reemplaza la lista completa de clases y `Remove("class")` la vacía.
|
||||||
|
///
|
||||||
|
/// Las variantes `*Style` gestionan las declaraciones de estilos para el atributo `style`, con una
|
||||||
|
/// propiedad cada vez. Además, `Set("style", ...)` reemplaza la lista completa de estilos y
|
||||||
|
/// `Remove("style")` la vacía.
|
||||||
///
|
///
|
||||||
/// Las variantes [`Set`](Self::Set) y [`Remove`](Self::Remove) son operaciones de propósito
|
/// Las variantes [`Set`](Self::Set) y [`Remove`](Self::Remove) son operaciones de propósito
|
||||||
/// general: `Set` añade o reemplaza cualquier atributo HTML por nombre y valor, y `Remove` lo
|
/// general. `Set` añade o reemplaza cualquier atributo HTML por nombre y valor, y `Remove` lo
|
||||||
/// elimina. Los atributos `id` y `class` tienen semántica especial documentada en cada variante.
|
/// elimina. Los atributos `id`, `class` y `style` tienen semántica especial documentada en cada
|
||||||
|
/// variante.
|
||||||
///
|
///
|
||||||
/// Las variantes `*Extra` permiten añadir valores tipados usando una clave. Están pensadas para que
|
/// Las variantes `*Extra` permiten añadir valores tipados usando una clave. Están pensadas para
|
||||||
/// temas y extensiones amplíen el comportamiento de componentes ya existentes. Como no es posible
|
/// ampliar el comportamiento de componentes ya existentes. Como no es posible añadir campos a la
|
||||||
/// añadir campos a la estructura de un componente ya definido, temas y extensiones pueden definir
|
/// estructura de un componente ya definido, temas y extensiones pueden definir un *trait* con
|
||||||
/// un *trait* con nuevos métodos que leen y escriben valores extra en [`Props`]. Esos valores se
|
/// nuevos métodos que leen y escriben valores extra en [`Props`]. Esos valores se interpretan como
|
||||||
/// interpretan como si fueran valores internos del componente para tomar decisiones durante el
|
/// si fueran valores internos del componente para tomar decisiones durante el renderizado.
|
||||||
/// renderizado.
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum PropsOp {
|
pub enum PropsOp {
|
||||||
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
|
/// Establece el identificador del componente normalizando el valor: recorta espacios, convierte
|
||||||
|
|
@ -120,13 +124,31 @@ pub enum PropsOp {
|
||||||
/// Elimina la clase o clases indicadas de la lista. La operación se ignora si el valor contiene
|
/// Elimina la clase o clases indicadas de la lista. La operación se ignora si el valor contiene
|
||||||
/// caracteres no ASCII.
|
/// caracteres no ASCII.
|
||||||
RemoveClasses(CowStr),
|
RemoveClasses(CowStr),
|
||||||
/// Añade un atributo o sustituye su valor si ya existe. Usar `"id"` como nombre de atributo
|
/// Añade una declaración de estilo (propiedad, valor) o sustituye su valor si la propiedad ya
|
||||||
/// aplica al valor la misma normalización que [`SetId`](Self::SetId). Usar `"class"` como
|
/// existe, conservando su posición; si no, se añade al final. A diferencia de las clases, el
|
||||||
/// nombre de atributo reemplaza la lista completa de clases por las nuevas indicadas; la
|
/// valor admite caracteres no ASCII (p. ej. `content`, `font-family`) y distingue mayúsculas y
|
||||||
/// operación se ignora si el valor contiene caracteres no ASCII.
|
/// minúsculas. El nombre de la propiedad se normaliza a minúsculas. Si la propiedad o el valor
|
||||||
|
/// quedan vacíos tras recortar espacios, la operación se ignora.
|
||||||
|
AddStyle(CowStr, CowStr),
|
||||||
|
/// Elimina la propiedad de estilo indicada, si existe.
|
||||||
|
RemoveStyle(CowStr),
|
||||||
|
/// Añade un atributo o sustituye su valor si ya existe.
|
||||||
|
///
|
||||||
|
/// Usar `"id"` como nombre de atributo aplica al valor la misma normalización que
|
||||||
|
/// [`SetId`](Self::SetId).
|
||||||
|
///
|
||||||
|
/// Usar `"class"` como nombre de atributo reemplaza la lista completa de clases por las nuevas
|
||||||
|
/// indicadas; la operación se ignora si el valor contiene caracteres no ASCII.
|
||||||
|
///
|
||||||
|
/// Usar `"style"` como nombre de atributo reemplaza la lista completa de estilos por los nuevos
|
||||||
|
/// indicados, interpretando el valor como declaraciones `"propiedad: valor"` separadas por `;`
|
||||||
|
/// (igual que el propio atributo `style` HTML). El separador `;` respeta paréntesis y comillas,
|
||||||
|
/// tal que valores como una *data URI* (`background: url(data:image/png;base64,...)`) o una
|
||||||
|
/// cadena con `;` (`content: "a;b"`) se interpretan correctamente. En cualquier caso, se
|
||||||
|
/// recomienda usar [`PropsOp::add_style()`](Self::add_style) para declarar estilos.
|
||||||
Set(CowStr, CowStr),
|
Set(CowStr, CowStr),
|
||||||
/// Elimina el atributo indicado. Usar `"id"` elimina el identificador; usar `"class"` vacía la
|
/// Elimina el atributo indicado. Usar `"id"` elimina el identificador; usar `"class"` vacía la
|
||||||
/// lista de clases.
|
/// lista de clases; y usar `"style"` vacía la lista de estilos.
|
||||||
Remove(CowStr),
|
Remove(CowStr),
|
||||||
/// Almacena un valor extra tipado asociado a la clave indicada. Si ya existe uno con esa clave,
|
/// Almacena un valor extra tipado asociado a la clave indicada. Si ya existe uno con esa clave,
|
||||||
/// lo reemplaza.
|
/// lo reemplaza.
|
||||||
|
|
@ -174,6 +196,26 @@ impl PropsOp {
|
||||||
Self::RemoveClasses(classes.into())
|
Self::RemoveClasses(classes.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`AddStyle`](Self::AddStyle) con la propiedad y el valor de estilo
|
||||||
|
/// indicados.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// let props = Props::default()
|
||||||
|
/// .with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
/// .with_prop(PropsOp::add_style("font-weight", "bold"))
|
||||||
|
/// .with_prop(PropsOp::add_style("color", "blue"));
|
||||||
|
/// assert_eq!(props.get_styles(), Some("color: blue; font-weight: bold".to_string()));
|
||||||
|
/// ```
|
||||||
|
pub fn add_style(property: impl Into<CowStr>, value: impl Into<CowStr>) -> Self {
|
||||||
|
Self::AddStyle(property.into(), value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea la variante [`RemoveStyle`](Self::RemoveStyle) para la propiedad de estilo indicada.
|
||||||
|
pub fn remove_style(property: impl Into<CowStr>) -> Self {
|
||||||
|
Self::RemoveStyle(property.into())
|
||||||
|
}
|
||||||
|
|
||||||
/// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo.
|
/// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo.
|
||||||
pub fn set(name: impl Into<CowStr>, value: impl Into<CowStr>) -> Self {
|
pub fn set(name: impl Into<CowStr>, value: impl Into<CowStr>) -> Self {
|
||||||
Self::Set(name.into(), value.into())
|
Self::Set(name.into(), value.into())
|
||||||
|
|
@ -212,9 +254,10 @@ impl PropsOp {
|
||||||
|
|
||||||
/// Recoge el identificador, clases CSS, atributos HTML y valores extra de un componente.
|
/// Recoge el identificador, clases CSS, atributos HTML y valores extra de un componente.
|
||||||
///
|
///
|
||||||
/// Al renderizar con [`html!`](crate::html::html) se emite primero el identificador `id` (si
|
/// Guarda estos valores con operaciones [`PropsOp`]. Cuando se renderiza usando
|
||||||
/// existe), luego `class` (si hay clases) y después el resto de atributos, normalmente aplicados al
|
/// [`html!`](crate::html::html) se emite primero el identificador `id` (si existe), luego `class`
|
||||||
/// elemento raíz del componente.
|
/// (si hay clases), después `style` (si hay declaraciones de estilo) y por último el resto de
|
||||||
|
/// atributos; normalmente se asignan al elemento raíz del componente.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -276,6 +319,23 @@ impl PropsOp {
|
||||||
/// assert_eq!(markup.into_string(), r#"<button class="btn btn-secondary active">OK</button>"#);
|
/// assert_eq!(markup.into_string(), r#"<button class="btn btn-secondary active">OK</button>"#);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
|
/// # Estilos CSS
|
||||||
|
///
|
||||||
|
/// Cada declaración se añade indicando una propiedad y su valor. Si la propiedad ya existe,
|
||||||
|
/// [`AddStyle`](PropsOp::AddStyle) sustituye su valor conservando la posición, sin duplicarla.
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// let props = Props::default()
|
||||||
|
/// .with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
/// .with_prop(PropsOp::add_style("font-weight", "bold"))
|
||||||
|
/// .with_prop(PropsOp::add_style("color", "blue"))
|
||||||
|
/// .with_prop(PropsOp::remove_style("font-weight"));
|
||||||
|
///
|
||||||
|
/// let markup = html! { button (props) { "OK" } };
|
||||||
|
/// assert_eq!(markup.into_string(), r#"<button style="color: blue">OK</button>"#);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
/// # Valores extra
|
/// # Valores extra
|
||||||
///
|
///
|
||||||
/// Las variantes [`SetExtra`](PropsOp::SetExtra) y [`RemoveExtra`](PropsOp::RemoveExtra), usando
|
/// Las variantes [`SetExtra`](PropsOp::SetExtra) y [`RemoveExtra`](PropsOp::RemoveExtra), usando
|
||||||
|
|
@ -341,6 +401,7 @@ impl PropsOp {
|
||||||
pub struct Props {
|
pub struct Props {
|
||||||
id: Option<String>,
|
id: Option<String>,
|
||||||
classes: Vec<String>,
|
classes: Vec<String>,
|
||||||
|
styles: Vec<(CowStr, CowStr)>,
|
||||||
attrs: Vec<(CowStr, CowStr)>,
|
attrs: Vec<(CowStr, CowStr)>,
|
||||||
extras: HashMap<&'static str, PropsExtra>,
|
extras: HashMap<&'static str, PropsExtra>,
|
||||||
}
|
}
|
||||||
|
|
@ -367,21 +428,7 @@ impl Props {
|
||||||
|
|
||||||
/// Modifica el identificador, las clases, los atributos o los valores extra según la operación
|
/// Modifica el identificador, las clases, los atributos o los valores extra según la operación
|
||||||
/// indicada. El método recomendado para construir cada operación es usar los constructores de
|
/// indicada. El método recomendado para construir cada operación es usar los constructores de
|
||||||
/// [`PropsOp`]:
|
/// [`PropsOp`].
|
||||||
///
|
|
||||||
/// - [`PropsOp::set_id()`] - establece el identificador normalizando el valor.
|
|
||||||
/// - [`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.
|
|
||||||
/// - [`PropsOp::remove()`] - elimina el atributo. `remove("id")` elimina el identificador;
|
|
||||||
/// `remove("class")` vacía la lista de clases.
|
|
||||||
/// - [`PropsOp::set_extra()`] - almacena un valor extra tipado.
|
|
||||||
/// - [`PropsOp::remove_extra()`] - elimina el valor extra asociado a la clave.
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
||||||
match op {
|
match op {
|
||||||
|
|
@ -445,6 +492,12 @@ impl Props {
|
||||||
.any(|r| r == c.as_str())
|
.any(|r| r == c.as_str())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
PropsOp::AddStyle(property, value) => {
|
||||||
|
self.set_style(property.as_ref(), value.as_ref());
|
||||||
|
}
|
||||||
|
PropsOp::RemoveStyle(property) => {
|
||||||
|
self.remove_style(property.as_ref());
|
||||||
|
}
|
||||||
PropsOp::Set(name, value) => {
|
PropsOp::Set(name, value) => {
|
||||||
if name.as_ref() == "id" {
|
if name.as_ref() == "id" {
|
||||||
self.apply_id(value.as_ref());
|
self.apply_id(value.as_ref());
|
||||||
|
|
@ -455,6 +508,9 @@ impl Props {
|
||||||
self.classes.clear();
|
self.classes.clear();
|
||||||
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
|
self.insert_classes(normalized.as_ref().split_ascii_whitespace(), 0);
|
||||||
}
|
}
|
||||||
|
} else if name.as_ref() == "style" {
|
||||||
|
self.styles.clear();
|
||||||
|
self.parse_styles(value.as_ref());
|
||||||
} else if let Some(pos) = self.attrs.iter().position(|(k, _)| k == &name) {
|
} else if let Some(pos) = self.attrs.iter().position(|(k, _)| k == &name) {
|
||||||
self.attrs[pos].1 = value;
|
self.attrs[pos].1 = value;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -466,6 +522,8 @@ impl Props {
|
||||||
self.id = None;
|
self.id = None;
|
||||||
} else if name.as_ref() == "class" {
|
} else if name.as_ref() == "class" {
|
||||||
self.classes.clear();
|
self.classes.clear();
|
||||||
|
} else if name.as_ref() == "style" {
|
||||||
|
self.styles.clear();
|
||||||
} else {
|
} else {
|
||||||
self.attrs.retain(|(k, _)| k != &name);
|
self.attrs.retain(|(k, _)| k != &name);
|
||||||
}
|
}
|
||||||
|
|
@ -497,14 +555,41 @@ impl Props {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve las declaraciones de estilo como cadena de texto (separadas por `"; "`), si hay
|
||||||
|
/// estilos definidos.
|
||||||
|
pub fn get_styles(&self) -> Option<String> {
|
||||||
|
if self.styles.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
self.styles
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| format!("{k}: {v}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("; "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el valor de la propiedad de estilo indicada, si existe.
|
||||||
|
pub fn get_style(&self, property: impl AsRef<str>) -> Option<String> {
|
||||||
|
let property = property.as_ref().trim().to_ascii_lowercase();
|
||||||
|
self.styles
|
||||||
|
.iter()
|
||||||
|
.find(|(k, _)| k.as_ref() == property)
|
||||||
|
.map(|(_, v)| v.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Devuelve el valor del atributo indicado, si existe.
|
/// Devuelve el valor del atributo indicado, si existe.
|
||||||
///
|
///
|
||||||
/// Los nombres `"id"` y `"class"` son equivalentes a llamar a [`get_id()`](Self::get_id) y
|
/// Los nombres `"id"`, `"class"` y `"style"` son equivalentes a llamar a
|
||||||
/// [`get_classes()`](Self::get_classes) respectivamente.
|
/// [`get_id()`](Self::get_id), [`get_classes()`](Self::get_classes) y
|
||||||
|
/// [`get_styles()`](Self::get_styles) respectivamente.
|
||||||
pub fn get_prop(&self, name: impl AsRef<str>) -> Option<String> {
|
pub fn get_prop(&self, name: impl AsRef<str>) -> Option<String> {
|
||||||
match name.as_ref() {
|
match name.as_ref() {
|
||||||
"id" => self.id.clone(),
|
"id" => self.id.clone(),
|
||||||
"class" => self.get_classes(),
|
"class" => self.get_classes(),
|
||||||
|
"style" => self.get_styles(),
|
||||||
name => self
|
name => self
|
||||||
.attrs
|
.attrs
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -525,18 +610,27 @@ impl Props {
|
||||||
self.classes.is_empty()
|
self.classes.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve `true` si no hay ningún estilo definido.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_styles_empty(&self) -> bool {
|
||||||
|
self.styles.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
/// Devuelve `true` si no hay ningún atributo adicional definido, sin tener en cuenta el
|
/// Devuelve `true` si no hay ningún atributo adicional definido, sin tener en cuenta el
|
||||||
/// identificador ni las clases.
|
/// identificador, las clases ni los estilos.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_attrs_empty(&self) -> bool {
|
pub fn is_attrs_empty(&self) -> bool {
|
||||||
self.attrs.is_empty()
|
self.attrs.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve `true` si no hay ningún identificador, clases ni atributos adicionales definidos,
|
/// Devuelve `true` si no hay ningún identificador, clases, estilos o atributos adicionales
|
||||||
/// sin tener en cuenta los valores extra.
|
/// definidos, sin tener en cuenta los valores extra.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.id.is_none() && self.attrs.is_empty() && self.classes.is_empty()
|
self.id.is_none()
|
||||||
|
&& self.classes.is_empty()
|
||||||
|
&& self.styles.is_empty()
|
||||||
|
&& self.attrs.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
|
/// Devuelve `true` si la clase o **todas** las clases indicadas están presentes.
|
||||||
|
|
@ -578,7 +672,10 @@ impl Props {
|
||||||
/// let props = Props::default().with_prop(PropsOp::set_extra(EXT_COUNT, 7_i32));
|
/// let props = Props::default().with_prop(PropsOp::set_extra(EXT_COUNT, 7_i32));
|
||||||
///
|
///
|
||||||
/// assert_eq!(*props.extra::<i32>(EXT_COUNT).unwrap(), 7);
|
/// assert_eq!(*props.extra::<i32>(EXT_COUNT).unwrap(), 7);
|
||||||
/// assert_eq!(props.extra::<i32>(EXT_OTHER), Err(PropsError::ExtraNotFound { key: EXT_OTHER }));
|
/// assert_eq!(
|
||||||
|
/// props.extra::<i32>(EXT_OTHER),
|
||||||
|
/// Err(PropsError::ExtraNotFound { key: EXT_OTHER })
|
||||||
|
/// );
|
||||||
/// assert!(matches!(
|
/// assert!(matches!(
|
||||||
/// props.extra::<u32>(EXT_COUNT),
|
/// props.extra::<u32>(EXT_COUNT),
|
||||||
/// Err(PropsError::ExtraTypeMismatch { .. })
|
/// Err(PropsError::ExtraTypeMismatch { .. })
|
||||||
|
|
@ -678,6 +775,79 @@ impl Props {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Añade o sustituye una declaración "propiedad: valor". Si la propiedad ya existe, sustituye
|
||||||
|
// su valor conservando la posición; si no, la añade al final. Ignora la declaración si la
|
||||||
|
// propiedad o el valor quedan vacíos tras recortar espacios. No aplica
|
||||||
|
// normalize_ascii_or_empty: ver la documentación de `PropsOp::AddStyle` sobre por qué los
|
||||||
|
// valores de estilo no se restringen a ASCII.
|
||||||
|
fn set_style(&mut self, property: &str, value: &str) {
|
||||||
|
let property = property.trim().to_ascii_lowercase();
|
||||||
|
let value = value.trim();
|
||||||
|
if property.is_empty() || value.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(pos) = self.styles.iter().position(|(k, _)| k.as_ref() == property) {
|
||||||
|
self.styles[pos].1 = value.to_string().into();
|
||||||
|
} else {
|
||||||
|
self.styles
|
||||||
|
.push((property.into(), value.to_string().into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpreta una cadena "propiedad: valor" separadas por ";" (igual que el atributo HTML
|
||||||
|
// `style`) y aplica cada declaración con `set_style`. Ignora las declaraciones sin ":".
|
||||||
|
fn parse_styles(&mut self, styles: &str) {
|
||||||
|
for style in Self::split_style_declarations(styles) {
|
||||||
|
let style = style.trim();
|
||||||
|
if style.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some((property, value)) = style.split_once(':') else {
|
||||||
|
trace::debug!(
|
||||||
|
target = "Props::with_prop",
|
||||||
|
declaration = %style,
|
||||||
|
"Ignoring malformed style declaration (missing \":\")"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
self.set_style(property, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divide una cadena de declaraciones de estilo por ";", igual que `str::split(';')`, pero sin
|
||||||
|
// cortar dentro de paréntesis (`url(...)`) ni de cadenas entre comillas simples o dobles
|
||||||
|
// (`content: "a;b"`). No es un análisis CSS completo: no reconoce comentarios `/* ... */` ni
|
||||||
|
// comillas escapadas, y unos paréntesis o comillas sin cerrar arrastran el resto de la cadena
|
||||||
|
// a la última declaración.
|
||||||
|
fn split_style_declarations(styles: &str) -> Vec<&str> {
|
||||||
|
let mut depth = 0i32;
|
||||||
|
let mut quote = None;
|
||||||
|
let mut start = 0;
|
||||||
|
let mut parts = Vec::new();
|
||||||
|
for (i, c) in styles.char_indices() {
|
||||||
|
if quote.is_none() && (c == '\'' || c == '"') {
|
||||||
|
quote = Some(c);
|
||||||
|
} else if quote == Some(c) {
|
||||||
|
quote = None;
|
||||||
|
} else if quote.is_none() && c == '(' {
|
||||||
|
depth += 1;
|
||||||
|
} else if quote.is_none() && c == ')' {
|
||||||
|
depth = (depth - 1).max(0);
|
||||||
|
} else if quote.is_none() && depth == 0 && c == ';' {
|
||||||
|
parts.push(&styles[start..i]);
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.push(&styles[start..]);
|
||||||
|
parts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Elimina la propiedad de estilo indicada, si existe.
|
||||||
|
fn remove_style(&mut self, property: &str) {
|
||||||
|
let property = property.trim().to_ascii_lowercase();
|
||||||
|
self.styles.retain(|(k, _)| k.as_ref() != property);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
|
@ -697,6 +867,15 @@ impl Render for Props {
|
||||||
}
|
}
|
||||||
w.push('"');
|
w.push('"');
|
||||||
}
|
}
|
||||||
|
if let Some((first, rest)) = self.styles.split_first() {
|
||||||
|
w.push_str(" style=\"");
|
||||||
|
let _ = write!(Escaper::new(w), "{}: {}", first.0, first.1);
|
||||||
|
for (property, value) in rest {
|
||||||
|
w.push_str("; ");
|
||||||
|
let _ = write!(Escaper::new(w), "{}: {}", property, value);
|
||||||
|
}
|
||||||
|
w.push('"');
|
||||||
|
}
|
||||||
for (name, value) in &self.attrs {
|
for (name, value) in &self.attrs {
|
||||||
w.push(' ');
|
w.push(' ');
|
||||||
let _ = write!(Escaper::new(w), "{}", name);
|
let _ = write!(Escaper::new(w), "{}", name);
|
||||||
|
|
|
||||||
317
tests/html_props_styles.rs
Normal file
317
tests/html_props_styles.rs
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
fn assert_styles(p: &Props, expected: Option<&str>) {
|
||||||
|
let got = p.get_styles();
|
||||||
|
assert_eq!(
|
||||||
|
got.as_deref(),
|
||||||
|
expected,
|
||||||
|
"Expected {:?}, got {:?}",
|
||||||
|
expected,
|
||||||
|
got
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< PropsOp::add_style >*************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn add_style_basic_adds_declaration() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style("color", "red"));
|
||||||
|
assert_styles(&p, Some("color: red"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn add_style_multiple_calls_accumulate_in_order() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
||||||
|
assert_styles(&p, Some("color: red; font-weight: bold"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn add_style_overrides_existing_property_preserving_position() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("font-weight", "bold"))
|
||||||
|
.with_prop(PropsOp::add_style("color", "blue"));
|
||||||
|
assert_styles(&p, Some("color: blue; font-weight: bold"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn add_style_property_name_is_case_insensitive() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("Color", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("COLOR", "blue"));
|
||||||
|
assert_styles(&p, Some("color: blue"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn add_style_value_preserves_case_and_non_ascii() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style("font-family", "'Alegreya Sans', ARIAL"));
|
||||||
|
assert_styles(&p, Some("font-family: 'Alegreya Sans', ARIAL"));
|
||||||
|
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style("content", "'Ñoño'"));
|
||||||
|
assert_styles(&p, Some("content: 'Ñoño'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn add_style_value_may_contain_semicolons() {
|
||||||
|
// A diferencia de PropsOp::Set("style", ...), que interpreta la cadena como declaraciones
|
||||||
|
// separadas por ";", AddStyle recibe la propiedad y el valor ya separados, así que un ";"
|
||||||
|
// dentro del valor (p. ej. una data URI) no supone ningún problema.
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style(
|
||||||
|
"background",
|
||||||
|
"url(data:image/png;base64,AAAA)",
|
||||||
|
));
|
||||||
|
assert_styles(&p, Some("background: url(data:image/png;base64,AAAA)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn add_style_trims_whitespace() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style(" color ", " red "));
|
||||||
|
assert_styles(&p, Some("color: red"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn add_style_ignores_empty_property_or_value() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("color", ""))
|
||||||
|
.with_prop(PropsOp::add_style(" ", " "));
|
||||||
|
assert_styles(&p, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< PropsOp::remove_style >**********************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn remove_style_removes_indicated_property() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("font-weight", "bold"))
|
||||||
|
.with_prop(PropsOp::remove_style("font-weight"));
|
||||||
|
assert_styles(&p, Some("color: red"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn remove_style_is_case_insensitive() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::remove_style("COLOR"));
|
||||||
|
assert_styles(&p, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn remove_style_trims_whitespace() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::remove_style(" color "));
|
||||||
|
assert_styles(&p, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn remove_style_non_existing_is_noop() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::remove_style("font-weight"));
|
||||||
|
assert_styles(&p, Some("color: red"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< PropsOp::set / remove ("style") >************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_replaces_entire_list() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("font-weight", "bold"))
|
||||||
|
.with_prop(PropsOp::set("style", "margin: 0"));
|
||||||
|
assert_styles(&p, Some("margin: 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_parses_multiple_declarations() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::set("style", "color: red; font-weight: bold"));
|
||||||
|
assert_styles(&p, Some("color: red; font-weight: bold"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_ignores_declaration_without_colon() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::set(
|
||||||
|
"style",
|
||||||
|
"color: red; not-a-declaration; font-weight: bold",
|
||||||
|
));
|
||||||
|
assert_styles(&p, Some("color: red; font-weight: bold"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_ignores_empty_property_or_value() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::set("style", ": red; color: ; ok: yes"));
|
||||||
|
assert_styles(&p, Some("ok: yes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_trims_whitespace_around_declarations() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::set(
|
||||||
|
"style",
|
||||||
|
" color : red ; font-weight:bold ",
|
||||||
|
));
|
||||||
|
assert_styles(&p, Some("color: red; font-weight: bold"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_with_empty_input_clears() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::set("style", ""));
|
||||||
|
assert_styles(&p, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_does_not_split_semicolon_inside_parens() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::set(
|
||||||
|
"style",
|
||||||
|
"background: url(data:image/png;base64,AAAA)",
|
||||||
|
));
|
||||||
|
assert_styles(&p, Some("background: url(data:image/png;base64,AAAA)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_does_not_split_semicolon_inside_quotes() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::set("style", r#"content: "a;b""#));
|
||||||
|
assert_styles(&p, Some(r#"content: "a;b""#));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_mixes_declarations_with_and_without_parens() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::set(
|
||||||
|
"style",
|
||||||
|
"color: red; background: url(a;b); margin: 0",
|
||||||
|
));
|
||||||
|
assert_styles(&p, Some("color: red; background: url(a;b); margin: 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Límite conocido de PropsOp::Set("style", ...): no es un análisis CSS completo. Unos paréntesis
|
||||||
|
// sin cerrar arrastran el resto de la cadena a la misma declaración. Este test fija el
|
||||||
|
// comportamiento actual para que un cambio futuro sea deliberado, no accidental.
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_reset_unbalanced_parens_swallows_rest_of_string() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::set(
|
||||||
|
"style",
|
||||||
|
"background: url(data:image/png; margin: 0",
|
||||||
|
));
|
||||||
|
assert_styles(&p, Some("background: url(data:image/png; margin: 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_remove_attr_clears_all_styles() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::remove("style"));
|
||||||
|
assert_styles(&p, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< is_styles_empty >****************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn props_is_styles_empty_on_default() {
|
||||||
|
assert!(Props::default().is_styles_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn props_is_styles_empty_false_after_add_style() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style("color", "red"));
|
||||||
|
assert!(!p.is_styles_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn props_is_styles_empty_true_after_remove_style() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::remove("style"));
|
||||||
|
assert!(p.is_styles_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< get_styles / get_style / get_prop("style") >*************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_get_returns_none_when_empty_some_when_not() {
|
||||||
|
assert_styles(&Props::default(), None);
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style("color", "red"));
|
||||||
|
assert_styles(&p, Some("color: red"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn get_style_returns_value_for_existing_property() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
||||||
|
assert_eq!(p.get_style("color"), Some("red".to_string()));
|
||||||
|
assert_eq!(p.get_style("font-weight"), Some("bold".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn get_style_is_case_insensitive_and_trims_input() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style("color", "red"));
|
||||||
|
assert_eq!(p.get_style("COLOR"), Some("red".to_string()));
|
||||||
|
assert_eq!(p.get_style(" Color "), Some("red".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn get_style_returns_none_for_missing_property() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style("color", "red"));
|
||||||
|
assert_eq!(p.get_style("margin"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn get_prop_style_matches_get_styles() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
||||||
|
assert_eq!(p.get_prop("style"), p.get_styles());
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< HTML rendering >*****************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn props_styles_renders_style_attribute() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::add_style("font-weight", "bold"));
|
||||||
|
assert_eq!(
|
||||||
|
html! { button (p) { "OK" } }.into_string(),
|
||||||
|
r#"<button style="color: red; font-weight: bold">OK</button>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn props_styles_render_after_class_and_before_other_attrs() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_id("main")
|
||||||
|
.with_prop(PropsOp::add_classes("btn"))
|
||||||
|
.with_prop(PropsOp::add_style("color", "red"))
|
||||||
|
.with_prop(PropsOp::set("data-x", "1"));
|
||||||
|
assert_eq!(
|
||||||
|
html! { button (p) { "OK" } }.into_string(),
|
||||||
|
r#"<button id="main" class="btn" style="color: red" data-x="1">OK</button>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn props_styles_escapes_double_quotes_in_value() {
|
||||||
|
let p = Props::default().with_prop(PropsOp::add_style("content", r#""hi""#));
|
||||||
|
assert_eq!(
|
||||||
|
html! { span (p) {} }.into_string(),
|
||||||
|
r#"<span style="content: "hi""></span>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Combined sequences >*************************************************************************
|
||||||
|
|
||||||
|
#[pagetop::test]
|
||||||
|
async fn styles_sequence_preserves_position_through_updates_and_removals() {
|
||||||
|
let p = Props::default()
|
||||||
|
.with_prop(PropsOp::add_style("color", "red")) // color: red
|
||||||
|
.with_prop(PropsOp::add_style("font-weight", "bold")) // color: red; font-weight: bold
|
||||||
|
.with_prop(PropsOp::add_style("margin", "0")) // color: red; font-weight: bold; margin: 0
|
||||||
|
.with_prop(PropsOp::remove_style("font-weight")) // color: red; margin: 0
|
||||||
|
.with_prop(PropsOp::add_style("color", "blue")); // color: blue; margin: 0
|
||||||
|
assert_styles(&p, Some("color: blue; margin: 0"));
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue