pagetop/extensions/pagetop-bootsier/src/theme/bs/form/input.rs
Manuel Cillero 1be3d88888 ♻️ Migra #[builder_fn] a #[builder_impl]
Sustituye las ~400 anotaciones `#[builder_fn]` método a método por un
único `#[builder_impl]` por `impl`/`trait`, en 74 ficheros de pagetop y
sus extensiones (admin, bootsier, menu, user).
2026-08-29 19:25:58 +02:00

124 lines
4.4 KiB
Rust

//! Definiciones para crear campos de texto de una línea.
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
/// superpone al control mientras está vacío y permanece flotante cuando tiene contenido o está
/// enfocado.
///
/// ```rust,no_run
/// # use pagetop::prelude::*;
/// use pagetop_bootsier::theme::*;
///
/// let nombre = bs::form::input::Field::text()
/// .with_name("name")
/// .with_label(Lc::n("Name"))
/// .with_placeholder(Lc::n("Enter your name"))
/// .with_floating_label(true);
/// ```
#[builder_impl]
pub trait InputBootsier {
/// Establece si la etiqueta se muestra flotante sobre el campo.
///
/// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene
/// contenido. Requiere que el campo tenga un atributo `placeholder` definido; si no se
/// especifica, se fuerza `placeholder=""` antes del renderizado.
fn with_floating_label(self, floating: bool) -> Self;
}
#[builder_impl]
impl InputBootsier for Field {
fn with_floating_label(mut self, floating: bool) -> Self {
self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating));
self
}
}
// **< 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<Markup, ComponentError> {
let container_id = field.id();
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
let input_class = if *field.plaintext() {
"form-control-plaintext"
} else {
"form-control"
};
let strict = field.kind().is_strict();
let masked = *field.kind() == Kind::StrictPassword;
let autocomplete = if strict {
Some(&form::Autocomplete::Off)
} else {
field.autocomplete()
};
// La etiqueta flotante requiere `placeholder` para animar la etiqueta.
let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false);
// 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=[Lc::l("field_required").lookup(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().as_deref()]
value=[field.value().as_deref()]
minlength=[field.minlength()]
maxlength=[field.maxlength()]
placeholder=[placeholder]
inputmode=[field.inputmode()]
autocomplete=[autocomplete]
spellcheck=[strict.then_some("false")]
autocorrect=[strict.then_some("off")]
style=[masked.then_some("-webkit-text-security: disc; text-security: disc;")]
autofocus[*field.autofocus()]
readonly[*field.readonly() || *field.plaintext() || strict]
onfocus=[strict.then_some("this.removeAttribute('readonly')")]
required[*field.required()]
disabled[*field.disabled()];
@if floating { (label) }
@if let Some(description) = field.help_text().lookup(cx) {
div class="form-text" { (description) }
}
}
})
}