From 87ad78778d05cf204121296e73bcda2e91ffa672 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Thu, 9 Jul 2026 01:26:52 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(bootsier):=20Mueve=20inpu?= =?UTF-8?q?t/select/textarea=20a=20theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolida los handlers de estos campos en la capa del tema. Además, `with_floating_label()` usa un valor extra que lee en `setup()` para forzar `multiple`/`rows` en 'select` y `textarea`. --- .../pagetop-bootsier/src/handlers/input.rs | 59 ------------ .../pagetop-bootsier/src/handlers/select.rs | 80 ---------------- .../pagetop-bootsier/src/handlers/textarea.rs | 63 ------------- extensions/pagetop-bootsier/src/lib.rs | 13 +-- .../src/theme/bs/form/input.rs | 78 ++++++++++++++- .../src/theme/bs/form/select.rs | 94 ++++++++++++++++++- .../src/theme/bs/form/textarea.rs | 77 ++++++++++++++- 7 files changed, 241 insertions(+), 223 deletions(-) delete mode 100644 extensions/pagetop-bootsier/src/handlers/input.rs delete mode 100644 extensions/pagetop-bootsier/src/handlers/select.rs delete mode 100644 extensions/pagetop-bootsier/src/handlers/textarea.rs diff --git a/extensions/pagetop-bootsier/src/handlers/input.rs b/extensions/pagetop-bootsier/src/handlers/input.rs deleted file mode 100644 index 94f906c4..00000000 --- a/extensions/pagetop-bootsier/src/handlers/input.rs +++ /dev/null @@ -1,59 +0,0 @@ -use pagetop::prelude::*; - -pub fn render(c: &form::input::Field, cx: &mut Context) -> Result { - let container_id = c.id(); - let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); - let floating = c.props().has_class("form-floating"); - let input_class = if *c.plaintext() { - "form-control-plaintext" - } else { - "form-control" - }; - // La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido, se - // fuerza `placeholder=""`. - let placeholder = if floating { - Some(c.placeholder().lookup(cx).unwrap_or_default()) - } else { - c.placeholder().lookup(cx) - }; - let label = match c.label().lookup(cx) { - Some(text) => html! { - label for=[input_id.as_deref()] class="form-label" { - (text) - @if *c.required() { - span - class="form-required" - title=(L10n::l("field_required").using(cx)) - { - "*" - } - } - } - }, - None => html! {}, - }; - Ok(html! { - div (c.props()) { - @if !floating { (label) } - input - type=(c.kind()) - id=[input_id.as_deref()] - class=(input_class) - name=[c.name().get()] - value=[c.value().get()] - minlength=[c.minlength().get()] - maxlength=[c.maxlength().get()] - placeholder=[placeholder] - inputmode=[c.inputmode().get()] - autocomplete=[c.autocomplete().get()] - autofocus[*c.autofocus()] - readonly[*c.readonly() || *c.plaintext()] - required[*c.required()] - disabled[*c.disabled()]; - @if floating { (label) } - @if let Some(description) = c.help_text().lookup(cx) { - div class="form-text" { (description) } - } - } - }) -} diff --git a/extensions/pagetop-bootsier/src/handlers/select.rs b/extensions/pagetop-bootsier/src/handlers/select.rs deleted file mode 100644 index 03a48735..00000000 --- a/extensions/pagetop-bootsier/src/handlers/select.rs +++ /dev/null @@ -1,80 +0,0 @@ -use pagetop::prelude::*; - -pub fn setup(c: &mut form::select::Field) { - if c.props().has_class("form-floating") { - c.alter_multiple(false); - c.alter_rows(None::); - } -} - -pub fn render(c: &form::select::Field, cx: &mut Context) -> Result { - let container_id = c.id(); - let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); - let floating = c.props().has_class("form-floating"); - let label = match c.label().lookup(cx) { - Some(text) => html! { - label for=[select_id.as_deref()] class="form-label" { - (text) - @if *c.required() { - span - class="form-required" - title=(L10n::l("field_required").using(cx)) - { - "*" - } - } - } - }, - None => html! {}, - }; - Ok(html! { - div (c.props()) { - @if !floating { (label) } - select - id=[select_id.as_deref()] - class="form-select" - name=[c.name().get()] - multiple[*c.multiple()] - size=[c.rows().get()] - autocomplete=[c.autocomplete().get()] - autofocus[*c.autofocus()] - required[*c.required()] - disabled[*c.disabled()] - { - @for entry in c.entries() { - @match entry { - form::select::Entry::Item(opt) => { - option - value=(opt.value().as_str().unwrap_or("")) - selected[*opt.selected()] - disabled[*opt.disabled()] - { - (opt.label().using(cx)) - } - } - form::select::Entry::Group(group) => { - optgroup - label=(group.label().using(cx)) - disabled[*group.disabled()] - { - @for opt in group.items() { - option - value=(opt.value().as_str().unwrap_or("")) - selected[*opt.selected()] - disabled[*opt.disabled()] - { - (opt.label().using(cx)) - } - } - } - } - } - } - } - @if floating { (label) } - @if let Some(description) = c.help_text().lookup(cx) { - div class="form-text" { (description) } - } - } - }) -} diff --git a/extensions/pagetop-bootsier/src/handlers/textarea.rs b/extensions/pagetop-bootsier/src/handlers/textarea.rs deleted file mode 100644 index 2f41a765..00000000 --- a/extensions/pagetop-bootsier/src/handlers/textarea.rs +++ /dev/null @@ -1,63 +0,0 @@ -use pagetop::prelude::*; - -pub fn setup(c: &mut form::Textarea) { - if c.props().has_class("form-floating") { - c.alter_rows(None::); - } -} - -pub fn render(c: &form::Textarea, cx: &mut Context) -> Result { - let container_id = c.id(); - let textarea_id = container_id - .as_deref() - .map(|id| util::join!(id, "-textarea")); - let floating = c.props().has_class("form-floating"); - // La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido se - // fuerza `placeholder=""`. - let placeholder = if floating { - Some(c.placeholder().lookup(cx).unwrap_or_default()) - } else { - c.placeholder().lookup(cx) - }; - let label = match c.label().lookup(cx) { - Some(text) => html! { - label for=[textarea_id.as_deref()] class="form-label" { - (text) - @if *c.required() { - span - class="form-required" - title=(L10n::l("field_required").using(cx)) - { - "*" - } - } - } - }, - None => html! {}, - }; - Ok(html! { - div (c.props()) { - @if !floating { (label) } - textarea - id=[textarea_id.as_deref()] - class="form-control" - name=[c.name().get()] - rows=[c.rows().get()] - minlength=[c.minlength().get()] - maxlength=[c.maxlength().get()] - placeholder=[placeholder] - autocomplete=[c.autocomplete().get()] - autofocus[*c.autofocus()] - readonly[*c.readonly()] - required[*c.required()] - disabled[*c.disabled()] - { - @if let Some(value) = c.value().get() { (value) } - } - @if floating { (label) } - @if let Some(description) = c.help_text().lookup(cx) { - div class="form-text" { (description) } - } - } - }) -} diff --git a/extensions/pagetop-bootsier/src/lib.rs b/extensions/pagetop-bootsier/src/lib.rs index 71253238..ae44ebbb 100644 --- a/extensions/pagetop-bootsier/src/lib.rs +++ b/extensions/pagetop-bootsier/src/lib.rs @@ -143,14 +143,15 @@ impl Theme for Bootsier { cx: &mut Context, ) -> Option> { setup_component!(component, { - Button => |c| handlers::button::setup(c), - form::select::Field => |c| handlers::select::setup(c), - form::Textarea => |c| handlers::textarea::setup(c), + Button => |c| theme::bs::button::setup(c), + form::input::Field => |c| theme::bs::form::input::setup(c), + form::select::Field => |c| theme::bs::form::select::setup(c), + form::Textarea => |c| theme::bs::form::textarea::setup(c), }); render_component!(component, { - form::input::Field => |c| handlers::input::render(c, cx), - form::select::Field => |c| handlers::select::render(c, cx), - form::Textarea => |c| handlers::textarea::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::Textarea => |c| theme::bs::form::textarea::render(c, cx), }) } diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/input.rs b/extensions/pagetop-bootsier/src/theme/bs/form/input.rs index 791358ac..4c85a1ce 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/input.rs @@ -4,6 +4,8 @@ use pagetop::prelude::*; pub use pagetop::base::component::form::input::{Field, Kind, Mode}; +const EXTRA_FLOATING_LABEL: &str = "bootsier.form.input.floating_label"; + /// Extensión de Bootsier para [`form::input::Field`]. /// /// Proporciona soporte para **etiquetas flotantes** (*floating label*). La etiqueta flotante se @@ -31,10 +33,76 @@ pub trait InputBootsier { impl InputBootsier for Field { fn with_floating_label(self, floating: bool) -> Self { - if floating { - self.with_prop(PropsOp::add_classes("form-floating")) - } else { - self.with_prop(PropsOp::remove_classes("form-floating")) - } + self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)) } } + +// **< Field SETUP >******************************************************************************** + +pub(crate) fn setup(field: &mut Field) { + if field.props().extra_or(EXTRA_FLOATING_LABEL, false) { + field.alter_prop(PropsOp::add_classes("form-floating")); + } else { + field.alter_prop(PropsOp::remove_classes("form-floating")); + } +} + +// **< Field RENDER >******************************************************************************* + +pub(crate) fn render(field: &Field, cx: &mut Context) -> Result { + let container_id = field.id(); + let input_id = container_id.as_deref().map(|id| util::join!(id, "-input")); + let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false); + let input_class = if *field.plaintext() { + "form-control-plaintext" + } else { + "form-control" + }; + // La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido, se + // fuerza `placeholder=""`. + let placeholder = if floating { + Some(field.placeholder().lookup(cx).unwrap_or_default()) + } else { + field.placeholder().lookup(cx) + }; + let label = match field.label().lookup(cx) { + Some(text) => html! { + label for=[input_id.as_deref()] class="form-label" { + (text) + @if *field.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + }, + None => html! {}, + }; + Ok(html! { + div (field.props()) { + @if !floating { (label) } + input + type=(field.kind()) + id=[input_id.as_deref()] + class=(input_class) + name=[field.name().get()] + value=[field.value().get()] + minlength=[field.minlength().get()] + maxlength=[field.maxlength().get()] + placeholder=[placeholder] + inputmode=[field.inputmode().get()] + autocomplete=[field.autocomplete().get()] + autofocus[*field.autofocus()] + readonly[*field.readonly() || *field.plaintext()] + required[*field.required()] + disabled[*field.disabled()]; + @if floating { (label) } + @if let Some(description) = field.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) +} diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/select.rs b/extensions/pagetop-bootsier/src/theme/bs/form/select.rs index 6e0b0378..76ffadf5 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/select.rs @@ -4,6 +4,8 @@ use pagetop::prelude::*; pub use pagetop::base::component::form::select::{Entry, Field, Group, Item}; +const EXTRA_FLOATING_LABEL: &str = "bootsier.form.select.floating_label"; + /// Extensión de Bootsier para [`form::select::Field`]. /// /// Proporciona soporte para **etiquetas flotantes** (*floating label*). La etiqueta flotante se @@ -36,10 +38,92 @@ pub trait SelectBootsier { impl SelectBootsier for Field { fn with_floating_label(self, floating: bool) -> Self { - if floating { - self.with_prop(PropsOp::add_classes("form-floating")) - } else { - self.with_prop(PropsOp::remove_classes("form-floating")) - } + self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)) } } + +// **< Field SETUP >******************************************************************************** + +pub(crate) fn setup(field: &mut Field) { + if field.props().extra_or(EXTRA_FLOATING_LABEL, false) { + field.alter_prop(PropsOp::add_classes("form-floating")); + field.alter_multiple(false); + field.alter_rows(None::); + } else { + field.alter_prop(PropsOp::remove_classes("form-floating")); + } +} + +// **< Field RENDER >******************************************************************************* + +pub(crate) fn render(field: &Field, cx: &mut Context) -> Result { + let container_id = field.id(); + let select_id = container_id.as_deref().map(|id| util::join!(id, "-select")); + let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false); + let label = match field.label().lookup(cx) { + Some(text) => html! { + label for=[select_id.as_deref()] class="form-label" { + (text) + @if *field.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + }, + None => html! {}, + }; + Ok(html! { + div (field.props()) { + @if !floating { (label) } + select + id=[select_id.as_deref()] + class="form-select" + name=[field.name().get()] + multiple[*field.multiple()] + size=[field.rows().get()] + autocomplete=[field.autocomplete().get()] + autofocus[*field.autofocus()] + required[*field.required()] + disabled[*field.disabled()] + { + @for entry in field.entries() { + @match entry { + form::select::Entry::Item(opt) => { + option + value=(opt.value().as_str().unwrap_or("")) + selected[*opt.selected()] + disabled[*opt.disabled()] + { + (opt.label().using(cx)) + } + } + form::select::Entry::Group(group) => { + optgroup + label=(group.label().using(cx)) + disabled[*group.disabled()] + { + @for opt in group.items() { + option + value=(opt.value().as_str().unwrap_or("")) + selected[*opt.selected()] + disabled[*opt.disabled()] + { + (opt.label().using(cx)) + } + } + } + } + } + } + } + @if floating { (label) } + @if let Some(description) = field.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) +} diff --git a/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs index 3f6796d8..04892cfb 100644 --- a/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs +++ b/extensions/pagetop-bootsier/src/theme/bs/form/textarea.rs @@ -4,6 +4,8 @@ use pagetop::prelude::*; pub use pagetop::base::component::form::Textarea; +const EXTRA_FLOATING_LABEL: &str = "bootsier.form.textarea.floating_label"; + /// Extensión de Bootsier para [`form::Textarea`]. /// /// Proporciona soporte para **etiquetas flotantes** (*floating label*). La etiqueta flotante se @@ -34,10 +36,75 @@ pub trait TextareaBootsier { impl TextareaBootsier for Textarea { fn with_floating_label(self, floating: bool) -> Self { - if floating { - self.with_prop(PropsOp::add_classes("form-floating")) - } else { - self.with_prop(PropsOp::remove_classes("form-floating")) - } + self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating)) } } + +// **< Textarea SETUP >***************************************************************************** + +pub(crate) fn setup(field: &mut Textarea) { + if field.props().extra_or(EXTRA_FLOATING_LABEL, false) { + field.alter_prop(PropsOp::add_classes("form-floating")); + field.alter_rows(None::); + } else { + field.alter_prop(PropsOp::remove_classes("form-floating")); + } +} + +// **< Textarea RENDER >**************************************************************************** + +pub(crate) fn render(field: &Textarea, cx: &mut Context) -> Result { + let container_id = field.id(); + let textarea_id = container_id + .as_deref() + .map(|id| util::join!(id, "-textarea")); + let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false); + // La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido se + // fuerza `placeholder=""`. + let placeholder = if floating { + Some(field.placeholder().lookup(cx).unwrap_or_default()) + } else { + field.placeholder().lookup(cx) + }; + let label = match field.label().lookup(cx) { + Some(text) => html! { + label for=[textarea_id.as_deref()] class="form-label" { + (text) + @if *field.required() { + span + class="form-required" + title=(L10n::l("field_required").using(cx)) + { + "*" + } + } + } + }, + None => html! {}, + }; + Ok(html! { + div (field.props()) { + @if !floating { (label) } + textarea + id=[textarea_id.as_deref()] + class="form-control" + name=[field.name().get()] + rows=[field.rows().get()] + minlength=[field.minlength().get()] + maxlength=[field.maxlength().get()] + placeholder=[placeholder] + autocomplete=[field.autocomplete().get()] + autofocus[*field.autofocus()] + readonly[*field.readonly()] + required[*field.required()] + disabled[*field.disabled()] + { + @if let Some(value) = field.value().get() { (value) } + } + @if floating { (label) } + @if let Some(description) = field.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) +}