diff --git a/assets/css/intro.css b/assets/css/intro.css index cc60b7b8..00fe0d21 100644 --- a/assets/css/intro.css +++ b/assets/css/intro.css @@ -389,10 +389,10 @@ body { .intro-text-body .block { position: relative; } -.intro-text-body .block-title { +.intro-text-body .block__title { margin: 1em 0 .8em; } -.intro-text-body .block-title span { +.intro-text-body .block__title span { display: inline-block; padding: 10px 30px 14px; margin: 30px 20px 0; @@ -403,7 +403,7 @@ body { border-color: orangered; transform: rotate(-3deg) translateY(-25%); } -.intro-text-body .block-title:before { +.intro-text-body .block__title:before { content: ""; height: 5px; position: absolute; @@ -416,7 +416,7 @@ body { transform: rotate(2deg) translateY(-50%); transform-origin: top left; } -.intro-text-body .block-title:after { +.intro-text-body .block__title:after { content: ""; height: 120%; position: absolute; @@ -427,22 +427,22 @@ body { background: var(--intro-bg-block-1); transform: rotate(2deg); } -.intro-text-body .block:nth-of-type(6n+1) .block-title:after { +.intro-text-body .block:nth-of-type(6n+1) .block__title:after { background: var(--intro-bg-block-1); } -.intro-text-body .block:nth-of-type(6n+2) .block-title:after { +.intro-text-body .block:nth-of-type(6n+2) .block__title:after { background: var(--intro-bg-block-2); } -.intro-text-body .block:nth-of-type(6n+3) .block-title:after { +.intro-text-body .block:nth-of-type(6n+3) .block__title:after { background: var(--intro-bg-block-3); } -.intro-text-body .block:nth-of-type(6n+4) .block-title:after { +.intro-text-body .block:nth-of-type(6n+4) .block__title:after { background: var(--intro-bg-block-4); } -.intro-text-body .block:nth-of-type(6n+5) .block-title:after { +.intro-text-body .block:nth-of-type(6n+5) .block__title:after { background: var(--intro-bg-block-5); } -.intro-text-body .block:nth-of-type(6n+6) .block-title:after { +.intro-text-body .block:nth-of-type(6n+6) .block__title:after { background: var(--intro-bg-block-6); } diff --git a/extensions/pagetop-bootsier/assets/_bootsier-custom.scss b/extensions/pagetop-bootsier/assets/_bootsier-custom.scss index 9a4c7e53..1f8421ec 100644 --- a/extensions/pagetop-bootsier/assets/_bootsier-custom.scss +++ b/extensions/pagetop-bootsier/assets/_bootsier-custom.scss @@ -18,11 +18,6 @@ font-display: swap; } -// Font-relative top offset to keep the skip-link hidden at any font size. -.skip-link { - top: -3em; -} - // Required field indicator in forms. .form-required { color: var(--bs-danger); diff --git a/extensions/pagetop-bootsier/src/theme/button.rs b/extensions/pagetop-bootsier/src/theme/button.rs index 17fb7cf6..fbb8617f 100644 --- a/extensions/pagetop-bootsier/src/theme/button.rs +++ b/extensions/pagetop-bootsier/src/theme/button.rs @@ -45,7 +45,9 @@ use crate::theme::{ButtonAction, ButtonColor, ButtonSize}; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Button { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del botón. props: Props, /// Devuelve el comportamiento del botón al activarse. kind: ButtonAction, @@ -71,7 +73,7 @@ impl Component for Button { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { @@ -84,6 +86,7 @@ impl Component for Button { fn prepare(&self, cx: &mut Context) -> Result { Ok(html! { button + id=[self.id()] type=(self.kind()) (self.props()) name=[self.name().get()] @@ -137,14 +140,14 @@ impl Button { // **< Button BUILDER >************************************************************************* - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del botón. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del botón. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/container/component.rs b/extensions/pagetop-bootsier/src/theme/container/component.rs index 07a835a7..bac350d0 100644 --- a/extensions/pagetop-bootsier/src/theme/container/component.rs +++ b/extensions/pagetop-bootsier/src/theme/container/component.rs @@ -20,7 +20,9 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Container { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del contenedor. props: Props, /// Devuelve el tipo semántico del contenedor. container_kind: container::Kind, @@ -36,7 +38,7 @@ impl Component for Container { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { @@ -56,32 +58,32 @@ impl Component for Container { }; Ok(match self.container_kind() { container::Kind::Default => html! { - div (self.props()) style=[style] { + div id=[self.id()] (self.props()) style=[style] { (output) } }, container::Kind::Main => html! { - main (self.props()) style=[style] { + main id=[self.id()] (self.props()) style=[style] { (output) } }, container::Kind::Header => html! { - header (self.props()) style=[style] { + header id=[self.id()] (self.props()) style=[style] { (output) } }, container::Kind::Footer => html! { - footer (self.props()) style=[style] { + footer id=[self.id()] (self.props()) style=[style] { (output) } }, container::Kind::Section => html! { - section (self.props()) style=[style] { + section id=[self.id()] (self.props()) style=[style] { (output) } }, container::Kind::Article => html! { - article (self.props()) style=[style] { + article id=[self.id()] (self.props()) style=[style] { (output) } }, @@ -132,14 +134,14 @@ impl Container { // **< Container BUILDER >********************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del contenedor. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del contenedor. /// /// También acepta clases predefinidas para: /// diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs index 88e3dcf9..779d9021 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/component.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/component.rs @@ -38,7 +38,9 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Dropdown { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del menú desplegable. props: Props, /// Devuelve el título del menú desplegable. title: L10n, @@ -68,7 +70,7 @@ impl Component for Dropdown { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { @@ -88,7 +90,7 @@ impl Component for Dropdown { let title = self.title().using(cx); Ok(html! { - div (self.props()) { + div id=[self.id()] (self.props()) { @if !title.is_empty() { @let btn_base = { let mut classes = "btn".to_string(); @@ -176,14 +178,14 @@ impl Component for Dropdown { impl Dropdown { // **< Dropdown BUILDER >*********************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del menú desplegable. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del menú desplegable. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs index 5eaecc00..53aec2e4 100644 --- a/extensions/pagetop-bootsier/src/theme/dropdown/item.rs +++ b/extensions/pagetop-bootsier/src/theme/dropdown/item.rs @@ -45,7 +45,9 @@ pub enum ItemKind { /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del elemento. props: Props, /// Devuelve el tipo de elemento representado. item_kind: ItemKind, @@ -57,7 +59,7 @@ impl Component for Item { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn prepare(&self, cx: &mut Context) -> Result { @@ -65,7 +67,7 @@ impl Component for Item { ItemKind::Void => html! {}, ItemKind::Label(label) => html! { - li (self.props()) { + li id=[self.id()] (self.props()) { span class="dropdown-item-text" { (label.using(cx)) } @@ -99,7 +101,7 @@ impl Component for Item { let tabindex = disabled.then_some("-1"); html! { - li (self.props()) { + li id=[self.id()] (self.props()) { a class=(classes) href=[href] @@ -125,7 +127,7 @@ impl Component for Item { let disabled_attr = disabled.then_some("disabled"); html! { - li (self.props()) { + li id=[self.id()] (self.props()) { button class=(classes) type="button" @@ -139,7 +141,7 @@ impl Component for Item { } ItemKind::Header(label) => html! { - li (self.props()) { + li id=[self.id()] (self.props()) { h6 class="dropdown-header" { (label.using(cx)) } @@ -147,7 +149,7 @@ impl Component for Item { }, ItemKind::Divider => html! { - li (self.props()) { hr class="dropdown-divider" {} } + li id=[self.id()] (self.props()) { hr class="dropdown-divider" {} } }, }) } @@ -258,14 +260,14 @@ impl Item { // **< Item BUILDER >*************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del elemento. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del elemento. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/check.rs b/extensions/pagetop-bootsier/src/theme/form/check.rs index b692cefd..7396dd2f 100644 --- a/extensions/pagetop-bootsier/src/theme/form/check.rs +++ b/extensions/pagetop-bootsier/src/theme/form/check.rs @@ -108,7 +108,9 @@ impl Item { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del contenedor del grupo. props: Props, /// Devuelve el nombre base compartido por todas las casillas del grupo. name: AttrName, @@ -130,31 +132,21 @@ impl Component for Field { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } - fn setup(&mut self, cx: &Context) { - // Asegura `name` e `id`. - // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. - let name = self - .name() - .get() - .unwrap_or_else(|| cx.required_id::(self.id(), 3)); - self.alter_name(&name); - let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); - self.alter_prop(PropsOp::ensure_id(container_id)); - - // Clases CSS del contenedor del grupo de casillas. + fn setup(&mut self, _cx: &Context) { self.alter_prop(PropsOp::prepend_classes("form-field form-field-checkboxes")); } fn prepare(&self, cx: &mut Context) -> Result { - // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. - let name = self.name().get().unwrap(); - let container_id = self.id().unwrap(); - + let name = self + .name() + .get() + .unwrap_or_else(|| cx.required_id::(self.id(), 3)); + let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); Ok(html! { - div (self.props()) { + div id=(&container_id) (self.props()) { @if let Some(label) = self.label().lookup(cx) { label class="form-label" { (label) } } @@ -196,14 +188,14 @@ impl Component for Field { impl Field { // **< Field BUILDER >************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del grupo de casillas. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del contenedor del grupo de casillas. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs index 74c3a1ff..06851900 100644 --- a/extensions/pagetop-bootsier/src/theme/form/checkbox.rs +++ b/extensions/pagetop-bootsier/src/theme/form/checkbox.rs @@ -43,7 +43,9 @@ use crate::theme::form; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Checkbox { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del contenedor del control. props: Props, /// Devuelve la variante visual del control. checkbox_kind: form::CheckboxKind, @@ -71,21 +73,10 @@ impl Component for Checkbox { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } - fn setup(&mut self, cx: &Context) { - // Asegura `name` e `id`. - // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. - let name = self - .name() - .get() - .unwrap_or_else(|| cx.required_id::(self.id(), 1)); - self.alter_name(&name); - let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); - self.alter_prop(PropsOp::ensure_id(container_id)); - - // Clases CSS del contenedor de la casilla de verificación. + fn setup(&mut self, _cx: &Context) { let mut classes = "form-field form-check".to_string(); if *self.checkbox_kind() == form::CheckboxKind::Switch { classes.push_str(" form-switch"); @@ -100,15 +91,15 @@ impl Component for Checkbox { } fn prepare(&self, cx: &mut Context) -> Result { - // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. - let name = self.name().get().unwrap(); - let container_id = self.id().unwrap(); - + let name = self + .name() + .get() + .unwrap_or_else(|| cx.required_id::(self.id(), 1)); + let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); let checkbox_id = util::join!(&container_id, "-checkbox"); let is_switch = *self.checkbox_kind() == form::CheckboxKind::Switch; - Ok(html! { - div (self.props()) { + div id=(&container_id) (self.props()) { input type="checkbox" role=[is_switch.then_some("switch")] @@ -154,14 +145,14 @@ impl Checkbox { // **< Checkbox BUILDER >*********************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del control. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del contenedor del control. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/component.rs b/extensions/pagetop-bootsier/src/theme/form/component.rs index 999d0dd0..9d6d2fad 100644 --- a/extensions/pagetop-bootsier/src/theme/form/component.rs +++ b/extensions/pagetop-bootsier/src/theme/form/component.rs @@ -47,7 +47,9 @@ use crate::theme::form; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Form { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del formulario. props: Props, /// Devuelve la URL/ruta de destino del formulario. action: AttrValue, @@ -66,7 +68,7 @@ impl Component for Form { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { @@ -80,6 +82,7 @@ impl Component for Form { }; Ok(html! { form + id=[self.id()] (self.props()) action=[self.action().get()] method=[method] @@ -94,14 +97,14 @@ impl Component for Form { impl Form { // **< Form BUILDER >*************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del formulario. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del formulario. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs index 5a880376..2e6b7493 100644 --- a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs +++ b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs @@ -24,7 +24,9 @@ use pagetop::prelude::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Fieldset { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del `fieldset`. props: Props, /// Devuelve la leyenda del `fieldset`. legend: Attr, @@ -42,7 +44,7 @@ impl Component for Fieldset { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn prepare(&self, cx: &mut Context) -> Result { @@ -53,7 +55,7 @@ impl Component for Fieldset { } Ok(html! { - fieldset (self.props()) disabled[*self.disabled()] { + fieldset id=[self.id()] (self.props()) disabled[*self.disabled()] { @if let Some(legend) = self.legend().lookup(cx) { legend { (legend) } } @@ -69,14 +71,14 @@ impl Component for Fieldset { impl Fieldset { // **< Fieldset BUILDER >*********************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del `fieldset` (grupo de controles). #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del `fieldset`. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs index d920a68c..a15f5e95 100644 --- a/extensions/pagetop-bootsier/src/theme/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -126,7 +126,9 @@ impl fmt::Display for Mode { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del contenedor del campo. props: Props, /// Devuelve el tipo de campo. kind: Kind, @@ -168,18 +170,10 @@ impl Component for Field { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { - if let Some(container_id) = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) - { - self.alter_prop(PropsOp::ensure_id(container_id)); - } - - // Clases CSS del contenedor del campo de texto. if *self.floating_label() { self.alter_prop(PropsOp::prepend_classes("form-floating")); } @@ -190,7 +184,9 @@ impl Component for Field { } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self.id(); + let container_id = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); let input_class = if *self.plaintext() { "form-control-plaintext" @@ -221,7 +217,7 @@ impl Component for Field { None => html! {}, }; Ok(html! { - div (self.props()) { + div id=[container_id.as_deref()] (self.props()) { @if !*self.floating_label() { (label) } @@ -317,14 +313,14 @@ impl Field { // **< Field BUILDER >************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del contenedor del campo. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del contenedor del campo. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/radio.rs b/extensions/pagetop-bootsier/src/theme/form/radio.rs index 76b2dc32..101fa2fd 100644 --- a/extensions/pagetop-bootsier/src/theme/form/radio.rs +++ b/extensions/pagetop-bootsier/src/theme/form/radio.rs @@ -96,7 +96,9 @@ impl Item { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del contenedor del grupo. props: Props, /// Devuelve el nombre compartido por todos los botones de opción del grupo. name: AttrName, @@ -120,31 +122,21 @@ impl Component for Field { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } - fn setup(&mut self, cx: &Context) { - // Asegura `name` e `id`. - // Si falta uno se deriva del otro; si faltan ambos se genera un valor único. - let name = self - .name() - .get() - .unwrap_or_else(|| cx.required_id::(self.id(), 3)); - self.alter_name(&name); - let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); - self.alter_prop(PropsOp::ensure_id(container_id)); - - // Clases CSS del contenedor del grupo de opciones. + fn setup(&mut self, _cx: &Context) { self.alter_prop(PropsOp::prepend_classes("form-field form-field-radios")); } fn prepare(&self, cx: &mut Context) -> Result { - // En `setup()` se garantiza que `name` e `id` están definidos antes del renderizado. - let name = self.name().get().unwrap(); - let container_id = self.id().unwrap(); - + let name = self + .name() + .get() + .unwrap_or_else(|| cx.required_id::(self.id(), 3)); + let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name)); Ok(html! { - div (self.props()) { + div id=(&container_id) (self.props()) { @if let Some(label) = self.label().lookup(cx) { label class="form-label" { (label) @@ -198,14 +190,14 @@ impl Component for Field { impl Field { // **< Field BUILDER >************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del grupo de opciones. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del contenedor del grupo de opciones. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/range.rs b/extensions/pagetop-bootsier/src/theme/form/range.rs index 37dfa68f..bac5a0f7 100644 --- a/extensions/pagetop-bootsier/src/theme/form/range.rs +++ b/extensions/pagetop-bootsier/src/theme/form/range.rs @@ -31,7 +31,9 @@ use pagetop::prelude::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Range { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del contenedor del control deslizante. props: Props, /// Devuelve el nombre del campo. name: AttrName, @@ -59,26 +61,20 @@ impl Component for Range { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { - if let Some(container_id) = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) - { - self.alter_prop(PropsOp::ensure_id(container_id)); - }; - - // Clases CSS del contenedor del control deslizante. self.alter_prop(PropsOp::prepend_classes("form-field form-field-range")); } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self.id(); + let container_id = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); let range_id = container_id.as_deref().map(|id| util::join!(id, "-range")); Ok(html! { - div (self.props()) { + div id=[container_id.as_deref()] (self.props()) { @if let Some(label) = self.label().lookup(cx) { label for=[range_id.as_deref()] class="form-label" { (label) } } @@ -104,14 +100,14 @@ impl Component for Range { impl Range { // **< Range BUILDER >************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del contenedor del control deslizante. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del contenedor del control deslizante. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/select.rs b/extensions/pagetop-bootsier/src/theme/form/select.rs index 47b411aa..48dc7127 100644 --- a/extensions/pagetop-bootsier/src/theme/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/form/select.rs @@ -191,7 +191,9 @@ pub enum Entry { /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Field { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del contenedor de la lista de selección. props: Props, /// Devuelve el nombre del campo. name: AttrName, @@ -223,18 +225,10 @@ impl Component for Field { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { - if let Some(container_id) = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) - { - self.alter_prop(PropsOp::ensure_id(container_id)); - }; - - // Clases CSS del contenedor de la lista de selección. if *self.floating_label() { self.alter_multiple(false); self.alter_rows(None::); @@ -244,7 +238,9 @@ impl Component for Field { } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self.id(); + let container_id = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); let label = match self.label().lookup(cx) { Some(text) => html! { @@ -263,7 +259,7 @@ impl Component for Field { None => html! {}, }; Ok(html! { - div (self.props()) { + div id=[container_id.as_deref()] (self.props()) { @if !*self.floating_label() { (label) } @@ -322,14 +318,14 @@ impl Component for Field { impl Field { // **< Field BUILDER >*************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del contenedor del campo. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del contenedor de la lista de selección. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/form/textarea.rs index f47bc139..4a9f9c9c 100644 --- a/extensions/pagetop-bootsier/src/theme/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/form/textarea.rs @@ -34,7 +34,9 @@ use crate::theme::form; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Textarea { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del contenedor del área de texto. props: Props, /// Devuelve el nombre del campo. name: AttrName, @@ -72,18 +74,10 @@ impl Component for Textarea { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { - if let Some(container_id) = self - .id() - .or_else(|| self.name().get().map(|n| util::join!("edit-", n))) - { - self.alter_prop(PropsOp::ensure_id(container_id)); - }; - - // Clases CSS del contenedor del área de texto. if *self.floating_label() { self.alter_rows(None::); self.alter_prop(PropsOp::prepend_classes("form-floating")); @@ -92,7 +86,9 @@ impl Component for Textarea { } fn prepare(&self, cx: &mut Context) -> Result { - let container_id = self.id(); + let container_id = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); let textarea_id = container_id .as_deref() .map(|id| util::join!(id, "-textarea")); @@ -120,7 +116,7 @@ impl Component for Textarea { None => html! {}, }; Ok(html! { - div (self.props()) { + div id=[container_id.as_deref()] (self.props()) { @if !*self.floating_label() { (label) } @@ -156,14 +152,14 @@ impl Component for Textarea { impl Textarea { // **< Textarea BUILDER >*********************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del contenedor del campo. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del contenedor del campo. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/icon.rs b/extensions/pagetop-bootsier/src/theme/icon.rs index 970a6cd4..5b3f64d3 100644 --- a/extensions/pagetop-bootsier/src/theme/icon.rs +++ b/extensions/pagetop-bootsier/src/theme/icon.rs @@ -15,7 +15,7 @@ pub enum IconKind { #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Icon { - /// Devuelve los atributos HTML y clases CSS del componente. + /// Devuelve los atributos HTML y clases CSS del icono. props: Props, icon_kind: IconKind, aria_label: AttrL10n, @@ -26,10 +26,6 @@ impl Component for Icon { Self::default() } - fn id(&self) -> Option { - self.props.get_id() - } - fn setup(&mut self, _cx: &Context) { if !matches!(self.icon_kind(), IconKind::None) { self.alter_prop(PropsOp::prepend_classes("icon")); @@ -102,14 +98,7 @@ impl Icon { // **< Icon 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) -> Self { - self.props.alter_id(id); - self - } - - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del icono. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/image/component.rs b/extensions/pagetop-bootsier/src/theme/image/component.rs index 8ab379e7..72a07ae3 100644 --- a/extensions/pagetop-bootsier/src/theme/image/component.rs +++ b/extensions/pagetop-bootsier/src/theme/image/component.rs @@ -13,7 +13,9 @@ use crate::theme::*; /// - Aplicar el texto alternativo `alt` con **localización** mediante [`L10n`]. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Image { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS de la imagen. props: Props, /// Devuelve las dimensiones de la imagen. size: image::Size, @@ -29,11 +31,10 @@ impl Component for Image { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { - // Clases CSS por defecto para la imagen, según el origen seleccionado. self.alter_prop(PropsOp::prepend_classes(self.source().to_class())); } @@ -45,6 +46,7 @@ impl Component for Image { image::Source::Logo(logo) => { return Ok(html! { span + id=[self.id()] (self.props()) style=[dimensions] role=[(!is_decorative).then_some("img")] @@ -63,6 +65,7 @@ impl Component for Image { img src=[source] alt=(alt_text) + id=[self.id()] (self.props()) style=[dimensions] {} }) @@ -77,14 +80,14 @@ impl Image { // **< Image BUILDER >************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) de la imagen. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS de la imagen. /// /// También acepta clases predefinidas para: /// diff --git a/extensions/pagetop-bootsier/src/theme/nav/component.rs b/extensions/pagetop-bootsier/src/theme/nav/component.rs index 7f792db3..e79e88aa 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/component.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/component.rs @@ -32,7 +32,9 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Nav { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del menú. props: Props, /// Devuelve el estilo visual seleccionado. nav_kind: nav::Kind, @@ -48,11 +50,10 @@ impl Component for Nav { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { - // Clases CSS por defecto para el menú, según el estilo y la distribución seleccionados. self.alter_prop(PropsOp::prepend_classes({ let mut classes = "nav".to_string(); self.nav_kind().push_class(&mut classes); @@ -68,7 +69,7 @@ impl Component for Nav { } Ok(html! { - ul (self.props()) { + ul id=[self.id()] (self.props()) { (items) } }) @@ -93,14 +94,14 @@ impl Nav { // **< Nav BUILDER >**************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del menú. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del menú. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/nav/item.rs b/extensions/pagetop-bootsier/src/theme/nav/item.rs index 9fbbc47d..5d2ef3de 100644 --- a/extensions/pagetop-bootsier/src/theme/nav/item.rs +++ b/extensions/pagetop-bootsier/src/theme/nav/item.rs @@ -78,7 +78,9 @@ impl ItemKind { /// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Item { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del elemento. props: Props, /// Devuelve el tipo de elemento representado. item_kind: ItemKind, @@ -90,7 +92,7 @@ impl Component for Item { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn setup(&mut self, _cx: &Context) { @@ -102,7 +104,7 @@ impl Component for Item { ItemKind::Void => html! {}, ItemKind::Label(label) => html! { - li (self.props()) { + li id=[self.id()] (self.props()) { span class="nav-link disabled" aria-disabled="true" { (label.using(cx)) } @@ -135,7 +137,7 @@ impl Component for Item { let aria_disabled = (*disabled).then_some("true"); html! { - li (self.props()) { + li id=[self.id()] (self.props()) { a class=(classes) href=[href] @@ -151,7 +153,7 @@ impl Component for Item { } ItemKind::Html(html) => html! { - li (self.props()) { + li id=[self.id()] (self.props()) { (html.render(cx)) } }, @@ -168,7 +170,7 @@ impl Component for Item { .unwrap_or_else(|| "Dropdown".to_string()) }); html! { - li (self.props()) { + li id=[self.id()] (self.props()) { a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" @@ -281,14 +283,14 @@ impl Item { // **< Item BUILDER >*************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del elemento. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del elemento. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs index 511ea805..9e5082b6 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/brand.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/brand.rs @@ -13,6 +13,8 @@ use crate::theme::*; /// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido. #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Brand { + #[getters(skip)] + id: AttrId, /// Devuelve la imagen de marca (si la hay). image: Embed, /// Devuelve el título de la identidad de marca. @@ -30,6 +32,10 @@ impl Component for Brand { Self::default() } + fn id(&self) -> Option { + self.id.get() + } + fn prepare(&self, cx: &mut Context) -> Result { let image = self.image().render(cx); let title = self.title().using(cx); @@ -50,6 +56,13 @@ impl Component for Brand { impl Brand { // **< Brand BUILDER >************************************************************************** + /// Establece el identificador único (`id`) de la marca. + #[builder_fn] + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); + self + } + /// Asigna o quita la imagen de marca. Si se pasa `None`, no se mostrará. #[builder_fn] pub fn with_image(mut self, image: Option) -> Self { diff --git a/extensions/pagetop-bootsier/src/theme/navbar/component.rs b/extensions/pagetop-bootsier/src/theme/navbar/component.rs index 8709a857..0438c981 100644 --- a/extensions/pagetop-bootsier/src/theme/navbar/component.rs +++ b/extensions/pagetop-bootsier/src/theme/navbar/component.rs @@ -136,7 +136,9 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas"; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Navbar { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS de la barra de navegación. props: Props, /// Devuelve el punto de ruptura configurado. expand: BreakPoint, @@ -154,14 +156,10 @@ impl Component for Navbar { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } - fn setup(&mut self, cx: &Context) { - // Asegura que la barra de navegación tiene un identificador único. - self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); - - // Clases CSS por defecto para la barra de navegación. + fn setup(&mut self, _cx: &Context) { self.alter_prop(PropsOp::prepend_classes({ let mut classes = "navbar".to_string(); self.expand().push_class(&mut classes, "navbar-expand", ""); @@ -200,11 +198,11 @@ impl Component for Navbar { return Ok(html! {}); } - // `setup()` garantiza que habrá un `id` antes de renderizar. - let id = self.id().unwrap(); + // Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas. + let id = cx.required_id::(self.id(), 1); Ok(html! { - nav (self.props()) { + nav id=(&id) (self.props()) { div class="container-fluid" { @match self.layout() { // Barra más sencilla: sólo contenido. @@ -337,14 +335,14 @@ impl Navbar { // **< Navbar BUILDER >************************************************************************* - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) de la barra de navegación. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS de la barra de navegación. /// /// También acepta clases predefinidas para: /// diff --git a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs index 1d9f2c48..481414fb 100644 --- a/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs +++ b/extensions/pagetop-bootsier/src/theme/offcanvas/component.rs @@ -43,7 +43,9 @@ use crate::theme::*; /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Offcanvas { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del panel. props: Props, /// Devuelve el título del panel. title: L10n, @@ -67,14 +69,10 @@ impl Component for Offcanvas { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } - fn setup(&mut self, cx: &Context) { - // Asegura que el panel tiene un identificador único. - self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); - - // Clases CSS por defecto para el panel. + fn setup(&mut self, _cx: &Context) { self.alter_prop(PropsOp::prepend_classes({ let mut classes = "offcanvas".to_string(); self.breakpoint().push_class(&mut classes, "offcanvas", ""); @@ -92,14 +90,14 @@ impl Component for Offcanvas { impl Offcanvas { // **< Offcanvas BUILDER >********************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del panel. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del panel. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); @@ -174,8 +172,7 @@ impl Offcanvas { return html! {}; } - // `setup()` garantiza que habrá un `id` antes de renderizar. - let id = self.id().unwrap(); + let id = cx.required_id::(self.id(), 1); let id_label = util::join!(id, "-label"); let id_target = util::join!("#", id); @@ -194,6 +191,7 @@ impl Offcanvas { html! { div + id=(&id) (self.props()) tabindex="-1" data-bs-scroll=[body_scroll] diff --git a/src/base/action/component/after_render_component.rs b/src/base/action/component/after_render_component.rs index 08eefdb5..0778e9c8 100644 --- a/src/base/action/component/after_render_component.rs +++ b/src/base/action/component/after_render_component.rs @@ -6,7 +6,7 @@ use super::FnActionWithComponent; pub struct AfterRender { f: FnActionWithComponent, referer_type_id: Option, - referer_id: Option, + referer_id: AttrId, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for AfterRender { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.clone() + self.referer_id.get() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl AfterRender { AfterRender { f, referer_type_id: Some(UniqueId::of::()), - referer_id: None, + referer_id: AttrId::default(), weight: 0, } } @@ -42,8 +42,7 @@ impl AfterRender { /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); - self.referer_id = if id.is_empty() { None } else { Some(id) }; + self.referer_id.alter_id(id); self } diff --git a/src/base/action/component/before_render_component.rs b/src/base/action/component/before_render_component.rs index e91589a2..051a3dd6 100644 --- a/src/base/action/component/before_render_component.rs +++ b/src/base/action/component/before_render_component.rs @@ -6,7 +6,7 @@ use super::FnActionWithComponent; pub struct BeforeRender { f: FnActionWithComponent, referer_type_id: Option, - referer_id: Option, + referer_id: AttrId, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for BeforeRender { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.clone() + self.referer_id.get() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl BeforeRender { BeforeRender { f, referer_type_id: Some(UniqueId::of::()), - referer_id: None, + referer_id: AttrId::default(), weight: 0, } } @@ -42,8 +42,7 @@ impl BeforeRender { /// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente /// `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); - self.referer_id = if id.is_empty() { None } else { Some(id) }; + self.referer_id.alter_id(id); self } diff --git a/src/base/action/component/transform_markup_component.rs b/src/base/action/component/transform_markup_component.rs index bed2e192..3e3a81f5 100644 --- a/src/base/action/component/transform_markup_component.rs +++ b/src/base/action/component/transform_markup_component.rs @@ -6,7 +6,7 @@ use super::FnActionTransformMarkup; pub struct TransformMarkup { f: FnActionTransformMarkup, referer_type_id: Option, - referer_id: Option, + referer_id: AttrId, weight: Weight, } @@ -19,7 +19,7 @@ impl ActionDispatcher for TransformMarkup { /// Devuelve el identificador del componente. fn referer_id(&self) -> Option { - self.referer_id.clone() + self.referer_id.get() } /// Devuelve el peso para definir el orden de ejecución. @@ -34,7 +34,7 @@ impl TransformMarkup { TransformMarkup { f, referer_type_id: Some(UniqueId::of::()), - referer_id: None, + referer_id: AttrId::default(), weight: 0, } } @@ -42,8 +42,7 @@ impl TransformMarkup { /// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el /// componente `C` con identificador `id`. pub fn filter_by_referer_id(mut self, id: impl AsRef) -> Self { - let id = id.as_ref().trim().to_ascii_lowercase().replace(' ', "_"); - self.referer_id = if id.is_empty() { None } else { Some(id) }; + self.referer_id.alter_id(id); self } diff --git a/src/base/component/block.rs b/src/base/component/block.rs index 2e56c5b0..b583313b 100644 --- a/src/base/component/block.rs +++ b/src/base/component/block.rs @@ -6,7 +6,9 @@ use crate::prelude::*; /// opcional y un cuerpo que sólo se renderiza si existen componentes hijos (*children*). #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Block { - /// Devuelve identificador, clases CSS y atributos HTML del componente. + #[getters(skip)] + id: AttrId, + /// Devuelve los atributos HTML y clases CSS del bloque. props: Props, /// Devuelve el título del bloque. title: L10n, @@ -20,15 +22,11 @@ impl Component for Block { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } - fn setup(&mut self, cx: &Context) { - // Asegura que el bloque tiene un identificador único. - self.alter_prop(PropsOp::ensure_id(cx.build_id::(1))); - - // Todos los bloques tienen la clase CSS `block` por defecto. - self.alter_prop(PropsOp::prepend_classes("block")); + fn setup(&mut self, _cx: &Context) { + self.props.alter_prop(PropsOp::prepend_classes("block")); } fn prepare(&self, cx: &mut Context) -> Result { @@ -38,12 +36,14 @@ impl Component for Block { return Ok(html! {}); } + let id = cx.required_id::(self.id(), 1); + Ok(html! { - div (self.props()) { + div id=(&id) (self.props()) { @if let Some(title) = self.title().lookup(cx) { - h2 class="block-title" { span { (title) } } + h2 class="block__title" { span { (title) } } } - div class="block-body" { (block_body) } + div class="block__body" { (block_body) } } }) } @@ -52,14 +52,14 @@ impl Component for Block { impl Block { // **< Block BUILDER >************************************************************************** - /// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`. + /// Establece el identificador único (`id`) del bloque. #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.props.alter_id(id); + pub fn with_id(mut self, id: impl AsRef) -> Self { + self.id.alter_id(id); self } - /// Modifica identificador, clases CSS o atributos HTML del componente. + /// Modifica los atributos HTML o las clases CSS del bloque. #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { self.props.alter_prop(op); diff --git a/src/core/component/context.rs b/src/core/component/context.rs index d27de662..f460b134 100644 --- a/src/core/component/context.rs +++ b/src/core/component/context.rs @@ -9,8 +9,8 @@ use crate::locale::{LangId, LanguageIdentifier, RequestLocale}; use crate::web::HttpRequest; use crate::{CowStr, builder_fn, util}; -use std::any::{Any, TypeId}; -use std::cell::RefCell; +use std::any::Any; +use std::cell::Cell; use std::collections::HashMap; use std::fmt; @@ -271,7 +271,7 @@ pub trait Contextual: LangId { /// assert_eq!(id, 42); /// /// // Genera un identificador para un componente de tipo `Menu`. -/// let unique_id = cx.build_id::(1); +/// let unique_id = cx.required_id::(None, 1); /// assert_eq!(unique_id, "menu-1"); // Si es el primero generado. /// } /// ``` @@ -286,7 +286,7 @@ pub struct Context { javascripts: Assets, // Scripts JavaScript. regions : ChildrenInRegions, // Regiones de componentes para renderizar. params : HashMap<&'static str, (Box, &'static str)>, // Parámetros en ejecución. - id_counters: RefCell>, // RefCell permite mutar desde build_id(&self). + id_counter : Cell, // Cell permite incrementar desde &self en required_id(). messages : Vec, // Mensajes de usuario acumulados. } @@ -314,7 +314,7 @@ impl Context { javascripts: Assets::::new(), regions : ChildrenInRegions::default(), params : HashMap::default(), - id_counters: RefCell::new(HashMap::new()), + id_counter : Cell::new(0), messages : Vec::new(), } } @@ -374,42 +374,31 @@ impl Context { route } - /// Construye un identificador HTML único para el tipo de componente `C`. + /// Garantiza un identificador único para un componente `C`, generándolo si no se proporciona + /// ninguno. /// - /// Toma los `segments` finales del *path* completo del tipo, los une con `-` y los convierte a - /// minúsculas, y añade un contador independiente por tipo. Por ejemplo, para `MyApp::ui::Menu` - /// con `segments = 2` devuelve `ui-menu-1` la primera vez que se invoca para ese tipo, - /// `ui-menu-2` la segunda, etc. + /// Si `id` es `None`, crea un identificador usando los últimos segmentos del *path* completo + /// del tipo `C`, separados por `-` y en minúsculas, seguidos de un contador incremental interno + /// del contexto. Por ejemplo, para un componente `MyApp::ui::Menu` con `parts = 2` podría + /// devolver un identificador como `ui-menu-1` si ha sido el primero en generarse. /// - /// Con `segments = 1` se usa sólo el nombre corto del tipo. Si `segments` es `0` o supera el - /// número de segmentos del *path*, se usan todos. + /// Con `parts = 1` se usa el nombre corto del tipo. Si `parts` es `0` o supera el número de + /// segmentos del *path*, entonces se usará el *path* completo. /// - /// Es útil para asignar identificadores cuando el componente no recibe uno explícito. El - /// contador es local a este contexto y se reinicia para cada nueva petición. - pub fn build_id(&self, segments: usize) -> String { - let path: Vec<&str> = TypeInfo::FullName.of::().split("::").collect(); - let segments = if segments == 0 || segments >= path.len() { - path.len() - } else { - segments - }; - let count = { - let mut map = self.id_counters.borrow_mut(); - let n = map.entry(TypeId::of::()).or_insert(0); - *n += 1; - *n - }; - let prefix = path[path.len() - segments..].join("-").to_lowercase(); - util::join!(prefix, "-", count.to_string()) - } - - /// Devuelve `id` si contiene un valor, o genera uno único con [`build_id`](Self::build_id) - /// si es `None`. - pub fn required_id(&self, id: Option, segments: usize) -> String { - match id { - Some(id) => id, - None => self.build_id::(segments), + /// Es útil para asignar identificadores HTML cuando el componente no recibe uno explícito. + pub fn required_id(&self, id: Option, parts: usize) -> String { + if let Some(id) = id { + return id; } + let segments: Vec<&str> = TypeInfo::FullName.of::().split("::").collect(); + let parts = if parts == 0 || parts >= segments.len() { + segments.len() + } else { + parts + }; + self.id_counter.set(self.id_counter.get() + 1); + let prefix = segments[segments.len() - parts..].join("-").to_lowercase(); + util::join!(prefix, "-", self.id_counter.get().to_string()) } /// Acumula un [`StatusMessage`] en el contexto para notificar al visitante. diff --git a/src/html.rs b/src/html.rs index 9f2daace..bff37698 100644 --- a/src/html.rs +++ b/src/html.rs @@ -20,7 +20,7 @@ pub use logo::PageTopSvg; // **< HTML ATTRIBUTES >**************************************************************************** mod attr; -pub use attr::{Attr, AttrName, AttrValue}; +pub use attr::{Attr, AttrId, AttrName, AttrValue}; mod props; pub use props::{Props, PropsOp}; diff --git a/src/html/attr.rs b/src/html/attr.rs index 07fe52f5..8f25a5eb 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -8,7 +8,7 @@ use crate::{AutoDefault, builder_fn}; /// /// Este tipo **no impone ninguna normalización ni semántica concreta**; dichas reglas se definen en /// implementaciones concretas como `Attr` y `Attr`, o en tipos específicos como -/// [`AttrName`]. +/// [`AttrId`] y [`AttrName`]. #[derive(AutoDefault, Clone, Debug)] pub struct Attr(Option); @@ -128,6 +128,73 @@ impl Attr { } } +// **< AttrId >************************************************************************************* + +/// Identificador normalizado para el atributo `id` o similar de HTML. +/// +/// Este tipo encapsula `Option` garantizando un valor normalizado para su uso: +/// +/// - Se eliminan los espacios al principio y al final. +/// - Se convierte a minúsculas. +/// - Se sustituyen los espacios (`' '`) intermedios por guiones bajos (`_`). +/// - Si el resultado es una cadena vacía, se guarda `None`. +/// +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// let id = AttrId::new(" main Section "); +/// assert_eq!(id.as_str(), Some("main_section")); +/// +/// let empty = AttrId::default(); +/// assert_eq!(empty.get(), None); +/// ``` +#[derive(AutoDefault, Clone, Debug)] +pub struct AttrId(Attr); + +impl AttrId { + /// Crea un nuevo `AttrId` normalizando el valor. + pub fn new(id: impl AsRef) -> Self { + Self::default().with_id(id) + } + + // **< AttrId BUILDER >************************************************************************* + + /// Establece un identificador nuevo normalizando el valor. + #[builder_fn] + pub fn with_id(mut self, id: impl AsRef) -> Self { + let id = id.as_ref().trim(); + if id.is_empty() { + self.0 = Attr::default(); + } else { + self.0 = Attr::some(id.to_ascii_lowercase().replace(' ', "_")); + } + self + } + + // **< AttrId GETTERS >************************************************************************* + + /// Devuelve el identificador normalizado, si existe. + pub fn get(&self) -> Option { + self.0.get() + } + + /// Devuelve el identificador normalizado (sin clonar), si existe. + pub fn as_str(&self) -> Option<&str> { + self.0.as_str() + } + + /// Devuelve el identificador normalizado (propiedad), si existe. + pub fn into_inner(self) -> Option { + self.0.into_inner() + } + + /// `true` si no hay valor. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + // **< AttrName >*********************************************************************************** /// Nombre normalizado para el atributo `name` o similar de HTML. diff --git a/src/html/props.rs b/src/html/props.rs index 223924af..83227045 100644 --- a/src/html/props.rs +++ b/src/html/props.rs @@ -7,57 +7,32 @@ use std::fmt::Write; /// Operaciones disponibles sobre atributos HTML y clases CSS en [`Props`]. /// -/// Cada variante lleva los datos necesarios para ejecutarse. El método recomendado para usarlas es -/// recurrir a los constructores asociados como [`set()`](Self::set), [`set_id()`](Self::set_id), -/// [`remove()`](Self::remove), [`add_classes()`](Self::add_classes), etc. +/// Cada variante es autocontenida, lleva todos los datos que necesita para ejecutarse. El método +/// recomendado para construirlas es usar los constructores asociados ([`set()`](Self::set), +/// [`remove()`](Self::remove), [`add_classes()`](Self::add_classes), etc.). /// -/// Las variantes `*Id` operan sobre el atributo `id` del componente. Cuando se usa `"id"` como -/// nombre de atributo en `Set`, el valor se normaliza igual que en `SetId` o `EnsureId`. +/// Las variantes `*Classes` operan siempre sobre la lista de clases CSS para el componente. /// -/// Las variantes `*Classes` operan siempre sobre la lista de clases CSS para el componente. Cuando -/// se usa `"class"` como nombre en `Set` o `Remove` la operación se aplica a la lista de clases -/// completa. Así, `Set("class", ...)` reemplaza la lista de clases completa por las nuevas clases -/// indicadas, y `Remove("class")` vacía la lista de clases. +/// Cuando se usa `"class"` como nombre de atributo en `Set` o `Remove` la operación se aplica a la +/// lista de clases completa. Así, `Set("class", ...)` reemplaza la lista de clases completa por las +/// nuevas clases indicadas, y `Remove("class")` vacía la lista de clases. #[derive(Clone, Debug, PartialEq)] pub enum PropsOp { - /// Establece el identificador del elemento normalizando el valor: recorta espacios, convierte a - /// minúsculas y sustituye los espacios intermedios por `_`. Si el resultado es vacío, elimina - /// el identificador. - SetId(CowStr), - /// Establece el identificador del elemento si aún no hay ninguno definido, de modo que no - /// sobrescribe un valor asignado con anterioridad. Aplica la misma normalización que - /// [`SetId`](Self::SetId); si el resultado es vacío, la operación tampoco tiene efecto. - EnsureId(CowStr), - /// Añade el atributo o sustituye su valor si ya existe. Usar `"id"` como nombre aplica la misma - /// normalización que [`SetId`](Self::SetId). Usar `"class"` como nombre reemplaza la lista - /// completa de clases por las nuevas indicadas; la operación se ignora si el valor contiene - /// caracteres no ASCII. + /// Añade el atributo o sustituye su valor si ya existe. Usar `"class"` como nombre reemplaza la + /// lista completa de clases por las nuevas indicadas; la operación se ignora si el valor + /// contiene caracteres no ASCII. Set(CowStr, CowStr), - /// Elimina el atributo indicado, incluido `"id"`. Si se usa `"class"` como nombre se vacía la - /// lista de clases. + /// Elimina el atributo indicado. Usar `"class"` como nombre vacía la lista de clases. Remove(CowStr), - /// Añade las clases que no existan al final de la lista. La operación se ignora si el valor - /// contiene caracteres no ASCII. + /// Añade las clases que no existan al final de la lista. AddClasses(CowStr), - /// Añade las clases que no existan al principio de la lista. La operación se ignora si el valor - /// contiene caracteres no ASCII. + /// Añade las clases que no existan al principio de la lista. PrependClasses(CowStr), - /// Elimina las clases indicadas de la lista. La operación se ignora si el valor contiene - /// caracteres no ASCII. + /// Elimina las clases indicadas de la lista. RemoveClasses(CowStr), } impl PropsOp { - /// Crea la variante [`SetId`](Self::SetId) con el identificador indicado. - pub fn set_id(id: impl Into) -> Self { - Self::SetId(id.into()) - } - - /// Crea la variante [`EnsureId`](Self::EnsureId) con el identificador indicado. - pub fn ensure_id(id: impl Into) -> Self { - Self::EnsureId(id.into()) - } - /// Crea la variante [`Set`](Self::Set) con nombre y valor del atributo. pub fn set(name: impl Into, value: impl Into) -> Self { Self::Set(name.into(), value.into()) @@ -86,10 +61,11 @@ impl PropsOp { // **< Props >************************************************************************************** -/// Colección de identificador, atributos HTML y clases CSS para aplicar en componentes. +/// Colección de pares `atributo="valor"` y clases CSS para aplicar en componentes. /// -/// Al renderizar en `html!` emite primero `id` (si existe), luego `class` (si hay clases) y después -/// el resto de atributos. +/// Permite añadir dinámicamente pares `atributo="valor"` y clases CSS al elemento raíz de un +/// componente. Al renderizar los atributos en `html!` primero emite el atributo `class` (si hay +/// clases) y luego el resto de atributos. /// /// # Ejemplo /// @@ -109,35 +85,6 @@ impl PropsOp { /// ); /// ``` /// -/// # Identificadores -/// -/// [`SetId`](PropsOp::SetId) (usando [`PropsOp::set_id`]) normaliza el valor asignado al -/// identificador del componente: recorta espacios, convierte a minúsculas y sustituye los espacios -/// intermedios por `_`. -/// -/// ```rust -/// # use pagetop::prelude::*; -/// let props = Props::default().with_id("My Button"); -/// let markup = html! { button (props) { "OK" } }; -/// assert_eq!(markup.into_string(), r#""#); -/// ``` -/// -/// [`EnsureId`](PropsOp::EnsureId) (usando [`PropsOp::ensure_id`]) sólo asigna si no -/// hay identificador previo: -/// -/// ```rust -/// # use pagetop::prelude::*; -/// // Con `id` previo: `EnsureId` no tiene efecto. -/// let props = Props::default() -/// .with_id("explicit") -/// .with_prop(PropsOp::ensure_id("default")); -/// assert_eq!(props.get_id(), Some("explicit".to_string())); -/// -/// // Sin `id` previo: `EnsureId` asigna el valor. -/// let props = Props::default().with_prop(PropsOp::ensure_id("default")); -/// assert_eq!(props.get_id(), Some("default".to_string())); -/// ``` -/// /// # Clases CSS /// /// ```rust @@ -175,7 +122,7 @@ impl PropsOp { /// } /// /// impl MyButton { -/// /// Modifica identificador, clases CSS o atributos HTML del elemento raíz. +/// /// Modifica los atributos HTML o las clases CSS del elemento raíz. /// #[builder_fn] /// pub fn with_prop(mut self, op: PropsOp) -> Self { /// self.props.alter_prop(op); @@ -185,7 +132,6 @@ impl PropsOp { /// ``` #[derive(AutoDefault, Clone, Debug)] pub struct Props { - id: Option, attrs: Vec<(CowStr, CowStr)>, classes: Vec, } @@ -203,23 +149,12 @@ impl Props { // **< Props BUILDER >************************************************************************** - /// Establece el identificador del componente; equivale a `with_prop(PropsOp::set_id(id))`. - #[builder_fn] - pub fn with_id(mut self, id: impl Into) -> Self { - self.apply_id(id.into().as_ref()); - self - } - - /// Modifica el identificador, los atributos o las clases según la operación indicada. + /// Modifica los atributos o clases según la operación indicada. /// - /// - [`SetId(value)`](PropsOp::SetId) establece el identificador normalizando el valor. - /// - [`EnsureId(value)`](PropsOp::EnsureId) establece el identificador (con la misma - /// normalización) sólo si no hay ninguno definido. /// - [`Set(name, value)`](PropsOp::Set) añade el atributo o reemplaza su valor. - /// `Set("id", ...)` aplica la misma normalización que `SetId`. /// `Set("class", ...)` reemplaza la lista de clases completa. - /// - [`Remove(name)`](PropsOp::Remove) elimina el atributo. `Remove("id")` elimina el - /// identificador. `Remove("class")` vacía la lista de clases. + /// - [`Remove(name)`](PropsOp::Remove) elimina el atributo. `Remove("class")` vacía la lista de + /// clases. /// - [`AddClasses(clases)`](PropsOp::AddClasses) añade clases al final (sin duplicados). /// - [`PrependClasses(clases)`](PropsOp::PrependClasses) añade clases al principio (sin /// duplicados). @@ -227,18 +162,8 @@ impl Props { #[builder_fn] pub fn with_prop(mut self, op: PropsOp) -> Self { match op { - PropsOp::SetId(value) => { - self.apply_id(value.as_ref()); - } - PropsOp::EnsureId(value) => { - if self.id.is_none() { - self.apply_id(value.as_ref()); - } - } PropsOp::Set(name, value) => { - if name.as_ref() == "id" { - self.apply_id(value.as_ref()); - } else if name.as_ref() == "class" { + if name.as_ref() == "class" { if let Some(normalized) = util::normalize_ascii_or_empty(value.as_ref(), "Props::with_prop") { @@ -252,9 +177,7 @@ impl Props { } } PropsOp::Remove(name) => { - if name.as_ref() == "id" { - self.id = None; - } else if name.as_ref() == "class" { + if name.as_ref() == "class" { self.classes.clear(); } else { self.attrs.retain(|(k, _)| k != &name); @@ -296,26 +219,18 @@ impl Props { // **< Props GETTERS >************************************************************************** - /// Devuelve el identificador normalizado del elemento, si existe. - #[inline] - pub fn get_id(&self) -> Option { - self.id.clone() + /// Devuelve el valor del atributo indicado, si existe. + pub fn get_prop(&self, name: impl AsRef) -> Option<&str> { + let name = name.as_ref(); + self.attrs + .iter() + .find(|(k, _)| k.as_ref() == name) + .map(|(_, v)| v.as_ref()) } - /// Devuelve el valor del atributo indicado, si existe. - /// - /// Los nombres `"id"` y `"class"` son equivalentes a llamar a [`get_id()`](Self::get_id) y - /// [`get_classes()`](Self::get_classes) respectivamente. - pub fn get_prop(&self, name: impl AsRef) -> Option { - match name.as_ref() { - "id" => self.id.clone(), - "class" => self.get_classes(), - name => self - .attrs - .iter() - .find(|(k, _)| k.as_ref() == name) - .map(|(_, v)| v.to_string()), - } + /// Devuelve `true` si no hay ningún atributo definido. + pub fn is_props_empty(&self) -> bool { + self.attrs.is_empty() } /// Devuelve la lista de clases como cadena de texto, si hay clases definidas. @@ -327,31 +242,11 @@ impl Props { } } - /// Devuelve `true` si no hay ningún identificador definido. - #[inline] - pub fn is_id_empty(&self) -> bool { - self.id.is_none() - } - - /// Devuelve `true` si no hay ningún atributo extra definido, sin tener en cuenta el - /// identificador ni las clases. - #[inline] - pub fn is_attrs_empty(&self) -> bool { - self.attrs.is_empty() - } - /// Devuelve `true` si no hay ninguna clase definida. - #[inline] pub fn is_classes_empty(&self) -> bool { self.classes.is_empty() } - /// Devuelve `true` si no hay ningún identificador, atributo ni clase definidos. - #[inline] - pub fn is_empty(&self) -> bool { - self.id.is_none() && self.attrs.is_empty() && self.classes.is_empty() - } - /// Devuelve `true` si la clase o **todas** las clases indicadas están presentes. pub fn has_class(&self, classes: impl AsRef) -> bool { let Ok(normalized) = util::normalize_ascii(classes.as_ref()) else { @@ -374,17 +269,9 @@ impl Props { .any(|class| self.classes.iter().any(|c| c == class)) } - // **< Props PRIVATE >************************************************************************** - - fn apply_id(&mut self, id: &str) { - let id = id.trim(); - self.id = if id.is_empty() { - None - } else { - Some(id.to_ascii_lowercase().replace(' ', "_")) - }; - } + // **< Props PRIVADO >************************************************************************** + #[inline] fn insert_classes<'a, I>(&mut self, classes: I, mut pos: usize) where I: IntoIterator, @@ -406,11 +293,6 @@ impl Props { #[doc(hidden)] impl Render for Props { fn render_to(&self, w: &mut String) { - if let Some(id) = self.id.as_deref() { - w.push_str(" id=\""); - let _ = write!(Escaper::new(w), "{}", id); - w.push('"'); - } if let Some((first, rest)) = self.classes.split_first() { w.push_str(" class=\""); let _ = write!(Escaper::new(w), "{}", first); diff --git a/src/response/page.rs b/src/response/page.rs index 57684365..4ca3f147 100644 --- a/src/response/page.rs +++ b/src/response/page.rs @@ -20,7 +20,7 @@ use crate::base::action; use crate::core::component::{AssetsOp, ChildOp, Context, ContextError, Contextual}; use crate::core::theme::{DefaultRegion, Region, RegionRef, TemplateRef, ThemeRef}; use crate::html::{Assets, Favicon, JavaScript, StyleSheet}; -use crate::html::{Attr, Props, PropsOp}; +use crate::html::{Attr, AttrId, Props, PropsOp}; use crate::html::{DOCTYPE, Markup, html}; use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier}; use crate::web::HttpRequest; @@ -89,6 +89,7 @@ pub struct Page { description : Attr, metadata : Vec<(&'static str, &'static str)>, properties : Vec<(&'static str, &'static str)>, + body_id : AttrId, body_props : Props, context : Context, } @@ -105,6 +106,7 @@ impl Page { description : Attr::::default(), metadata : Vec::default(), properties : Vec::default(), + body_id : AttrId::default(), body_props : Props::default(), context : Context::new(Some(request)), } @@ -140,7 +142,14 @@ impl Page { self } - /// Modifica identificador, clases CSS o atributos HTML del elemento ``. + /// Establece el atributo `id` del elemento ``. + #[builder_fn] + pub fn with_body_id(mut self, id: impl AsRef) -> Self { + self.body_id.alter_id(id); + self + } + + /// Modifica los atributos HTML o las clases CSS del elemento ``. #[builder_fn] pub fn with_body_props(mut self, op: PropsOp) -> Self { self.body_props.alter_prop(op); @@ -169,7 +178,12 @@ impl Page { &self.properties } - /// Devuelve identificador, clases CSS y atributos HTML del elemento ``. + /// Devuelve el identificador del elemento ``. + pub fn body_id(&self) -> &AttrId { + &self.body_id + } + + /// Devuelve los atributos HTML y clases CSS del elemento ``. pub fn body_props(&self) -> &Props { &self.body_props } @@ -247,7 +261,7 @@ impl Page { head { (head) } - body (self.body_props()) { + body id=[self.body_id().get()] (self.body_props()) { (body) } } diff --git a/tests/component_children.rs b/tests/component_children.rs index dc5eb5cc..791d23ff 100644 --- a/tests/component_children.rs +++ b/tests/component_children.rs @@ -7,7 +7,7 @@ use pagetop::prelude::*; #[derive(AutoDefault, Clone)] struct TestComp { - props: Props, + id: AttrId, text: String, } @@ -17,7 +17,7 @@ impl Component for TestComp { } fn id(&self) -> Option { - self.props.get_id() + self.id.get() } fn prepare(&self, _cx: &mut Context) -> Result { @@ -29,7 +29,7 @@ impl TestComp { /// Crea un componente con id y texto de salida fijos. fn tagged(id: &str, text: &str) -> Self { let mut c = Self::default(); - c.props.alter_prop(PropsOp::set_id(id.to_string())); + c.id.alter_id(id); c.text = text.to_string(); c } @@ -303,8 +303,7 @@ async fn embed_get_allows_mutating_component() { let embed = Embed::with(TestComp::tagged("orig", "texto")); // El `;` final convierte el `if let` en sentencia y libera el guard antes que `embed`. if let Some(mut comp) = embed.get() { - comp.props - .alter_prop(PropsOp::set_id("modificado".to_string())); + comp.id.alter_id("modificado"); }; assert_eq!(embed.id(), Some("modificado".to_string())); } @@ -332,8 +331,7 @@ async fn embed_clone_is_deep() { let clone = original.clone(); // Mutar el clon no debe afectar al original. if let Some(mut comp) = clone.get() { - comp.props - .alter_prop(PropsOp::set_id("clone-id".to_string())); + comp.id.alter_id("clone-id"); } assert_eq!(original.id(), Some("orig".to_string())); assert_eq!(clone.id(), Some("clone-id".to_string())); diff --git a/tests/html_props.rs b/tests/html_props.rs index 3db26746..f622dd95 100644 --- a/tests/html_props.rs +++ b/tests/html_props.rs @@ -13,7 +13,7 @@ async fn props_default_renders_nothing() { #[pagetop::test] async fn props_new_creates_first_attr() { let p = Props::new("hx-get", "/api"); - assert_eq!(p.get_prop("hx-get"), Some("/api".to_string())); + assert_eq!(p.get_prop("hx-get"), Some("/api")); } #[pagetop::test] @@ -59,14 +59,14 @@ async fn props_set_adds_new_attrs() { let p = Props::default() .with_prop(PropsOp::set("hx-get", "/api")) .with_prop(PropsOp::set("hx-swap", "outerHTML")); - assert_eq!(p.get_prop("hx-get"), Some("/api".to_string())); - assert_eq!(p.get_prop("hx-swap"), Some("outerHTML".to_string())); + assert_eq!(p.get_prop("hx-get"), Some("/api")); + assert_eq!(p.get_prop("hx-swap"), Some("outerHTML")); } #[pagetop::test] async fn props_set_replaces_existing_value() { let p = Props::new("hx-get", "/old").with_prop(PropsOp::set("hx-get", "/new")); - assert_eq!(p.get_prop("hx-get"), Some("/new".to_string())); + assert_eq!(p.get_prop("hx-get"), Some("/new")); } #[pagetop::test] @@ -98,13 +98,13 @@ async fn props_remove_existing_attr() { .with_prop(PropsOp::set("b", "2")) .with_prop(PropsOp::remove("a")); assert_eq!(p.get_prop("a"), None); - assert_eq!(p.get_prop("b"), Some("2".to_string())); + assert_eq!(p.get_prop("b"), Some("2")); } #[pagetop::test] async fn props_remove_nonexistent_key_is_noop() { let p = Props::new("a", "1").with_prop(PropsOp::remove("missing")); - assert_eq!(p.get_prop("a"), Some("1".to_string())); + assert_eq!(p.get_prop("a"), Some("1")); assert_eq!(p.get_prop("missing"), None); } @@ -222,42 +222,22 @@ async fn props_splice_empty_string_emits_nothing() { assert_eq!(html! { span ("") { "x" } }.into_string(), "x"); } -// **< is_attrs_empty / is_classes_empty / is_empty >*********************************************** +// **< is_props_empty / is_classes_empty >********************************************************** #[pagetop::test] -async fn props_is_attrs_empty_on_default() { - assert!(Props::default().is_attrs_empty()); +async fn props_is_props_empty_on_default() { + assert!(Props::default().is_props_empty()); } #[pagetop::test] -async fn props_is_attrs_empty_false_after_set() { - assert!(!Props::new("hx-get", "/api").is_attrs_empty()); +async fn props_is_props_empty_false_after_set() { + assert!(!Props::new("hx-get", "/api").is_props_empty()); } #[pagetop::test] -async fn props_is_attrs_empty_true_after_removing_last_attr() { +async fn props_is_props_empty_true_after_removing_last_attr() { let p = Props::new("only", "one").with_prop(PropsOp::remove("only")); - assert!(p.is_attrs_empty()); -} - -#[pagetop::test] -async fn props_is_empty_on_default() { - assert!(Props::default().is_empty()); -} - -#[pagetop::test] -async fn props_is_empty_false_with_id() { - assert!(!Props::default().with_id("main").is_empty()); -} - -#[pagetop::test] -async fn props_is_empty_false_with_attr() { - assert!(!Props::new("hx-get", "/api").is_empty()); -} - -#[pagetop::test] -async fn props_is_empty_false_with_class() { - assert!(!Props::classes("btn").is_empty()); + assert!(p.is_props_empty()); } #[pagetop::test] @@ -276,45 +256,6 @@ async fn props_is_classes_empty_true_after_remove_class() { assert!(p.is_classes_empty()); } -// **< get_prop("id") / get_prop("class") >********************************************************* - -#[pagetop::test] -async fn get_prop_id_returns_none_by_default() { - assert_eq!(Props::default().get_prop("id"), None); -} - -#[pagetop::test] -async fn get_prop_id_returns_normalized_value() { - let p = Props::default().with_id("My Button"); - assert_eq!(p.get_prop("id"), Some("my_button".to_string())); -} - -#[pagetop::test] -async fn get_prop_id_matches_get_id() { - let p = Props::default().with_id("Header"); - assert_eq!(p.get_prop("id"), p.get_id()); -} - -#[pagetop::test] -async fn get_prop_class_returns_none_by_default() { - assert_eq!(Props::default().get_prop("class"), None); -} - -#[pagetop::test] -async fn get_prop_class_returns_joined_classes() { - let p = Props::classes("btn btn-primary").with_prop(PropsOp::add_classes("active")); - assert_eq!( - p.get_prop("class"), - Some("btn btn-primary active".to_string()) - ); -} - -#[pagetop::test] -async fn get_prop_class_matches_get_classes() { - let p = Props::classes("btn active"); - assert_eq!(p.get_prop("class"), p.get_classes()); -} - // **< Regression & edge cases >******************************************************************** #[pagetop::test] @@ -343,9 +284,9 @@ async fn props_chained_set_and_remove_yields_expected_state() { .with_prop(PropsOp::set("c", "3")) .with_prop(PropsOp::remove("b")) .with_prop(PropsOp::set("a", "updated")); - assert_eq!(p.get_prop("a"), Some("updated".to_string())); + assert_eq!(p.get_prop("a"), Some("updated")); assert_eq!(p.get_prop("b"), None); - assert_eq!(p.get_prop("c"), Some("3".to_string())); + assert_eq!(p.get_prop("c"), Some("3")); assert_eq!( html! { span (p) {} }.into_string(), r#""#