pagetop/extensions/pagetop-bootsier/src/theme/bs/form/input.rs
Manuel Cillero c49bf8f56a (form): Blinda Field contra el autorrelleno
Añade nuevas variantes `Kind::StrictText` y `StrictPassword`
(`Field::strict_text()` y `strict_password()`) para forzar en el campo
`autocomplete=off`, desactivar spellcheck y autocorrect, aplicar
`readonly+onfocus`, y enmascarar las contraseñas vía CSS en vez de
`type="password"`. Replicado en pagetop-bootsier.
2026-08-02 08:43:32 +02:00

124 lines
4.5 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(L10n::n("Name"))
/// .with_placeholder(L10n::n("Enter your name"))
/// .with_floating_label(true);
/// ```
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.
#[builder_fn]
fn with_floating_label(self, floating: bool) -> Self;
}
impl InputBootsier for Field {
#[builder_fn]
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().get()
};
// 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=(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=[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) }
}
}
})
}