(bootsier): Añade campos de texto de una línea

This commit is contained in:
Manuel Cillero 2026-04-27 18:19:45 +02:00
parent 9273be2110
commit f85f2ea2d1
5 changed files with 377 additions and 143 deletions

View file

@ -1,8 +1,9 @@
//! Definiciones para crear formularios ([`Form`]). //! Definiciones para crear formularios ([`Form`]).
mod props; mod props;
pub use props::Method;
pub use props::{Autocomplete, AutofillField}; pub use props::{Autocomplete, AutofillField};
pub use props::{CheckboxKind, InputType, Method}; pub use props::CheckboxKind;
mod component; mod component;
pub use component::Form; pub use component::Form;
@ -10,9 +11,6 @@ pub use component::Form;
mod fieldset; mod fieldset;
pub use fieldset::Fieldset; pub use fieldset::Fieldset;
mod input;
pub use input::Input;
mod checkbox; mod checkbox;
pub use checkbox::Checkbox; pub use checkbox::Checkbox;
@ -21,3 +19,5 @@ pub mod check;
pub mod radio; pub mod radio;
pub mod select; pub mod select;
pub mod input;

View file

@ -19,8 +19,8 @@ use pagetop::prelude::*;
/// let personal_data = form::Fieldset::new() /// let personal_data = form::Fieldset::new()
/// .with_legend(L10n::n("Personal data")) /// .with_legend(L10n::n("Personal data"))
/// .with_description(L10n::n("Enter your full name and contact email.")) /// .with_description(L10n::n("Enter your full name and contact email."))
/// .with_child(form::Input::textfield().with_name("name").with_label(L10n::n("Full name"))) /// .with_child(form::input::Field::text().with_name("name").with_label(L10n::n("Full name")))
/// .with_child(form::Input::email().with_name("email").with_label(L10n::n("Email"))); /// .with_child(form::input::Field::email().with_name("email").with_label(L10n::n("Email")));
/// ``` /// ```
#[derive(AutoDefault, Clone, Debug, Getters)] #[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Fieldset { pub struct Fieldset {

View file

@ -1,47 +1,197 @@
//! Definiciones para crear campos de texto de una línea.
use pagetop::prelude::*; use pagetop::prelude::*;
use crate::theme::form; use crate::theme::form;
use crate::LOCALES_BOOTSIER; use crate::LOCALES_BOOTSIER;
#[derive(AutoDefault, Clone, Debug, Getters)] use std::fmt;
pub struct Input {
classes: Classes, // **< Kind >***************************************************************************************
input_type: form::InputType,
name: AttrName, /// Tipo de campo para un [`form::input::Field`].
value: AttrValue, ///
label: Attr<L10n>, /// Determina el tipo de entrada que acepta, así como el comportamiento del navegador al interactuar
help_text: Attr<L10n>, /// con el campo. Implícitamente se aplica al crear el control: [`text()`](Field::text),
#[default(_code = "Attr::<u16>::some(60)")] /// [`password()`](Field::password), [`search()`](Field::search), [`email()`](Field::email),
size: Attr<u16>, /// [`telephone()`](Field::telephone) o [`url()`](Field::url).
minlength: Attr<u16>, #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
#[default(_code = "Attr::<u16>::some(128)")] pub enum Kind {
maxlength: Attr<u16>, /// Entrada de texto genérico (`type="text"`). Es el tipo por defecto.
placeholder: AttrValue, #[default]
autocomplete: Attr<form::Autocomplete>, Text,
autofocus: bool, /// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado.
readonly: bool, Password,
required: bool, /// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda.
disabled: bool, Search,
/// Entrada de un correo electrónico (`type="email"`). Permite validar el formato del correo.
Email,
/// Entrada de un teléfono (`type="tel"`). Activa el teclado de llamadas en móviles.
Telephone,
/// Entrada de una URL (`type="url"`). Comprueba que la entrada sea una URL bien formada.
Url,
} }
impl Component for Input { impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Kind::Text => "text",
Kind::Password => "password",
Kind::Search => "search",
Kind::Email => "email",
Kind::Telephone => "tel",
Kind::Url => "url",
})
}
}
// **< Mode >***************************************************************************************
/// Sugerencia para el teclado virtual de un [`form::input::Field`].
///
/// Indica al navegador qué tipo de teclado virtual mostrar en dispositivos móviles o táctiles al
/// editar el campo. A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los
/// valores aceptados ni activa la validación del navegador; es sólo una sugerencia de presentación.
///
/// Se establece con [`form::input::Field::with_inputmode()`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Mode {
/// Suprime el teclado virtual. Útil en campos con teclado personalizado basado en JavaScript.
None,
/// Teclado de texto genérico.
Text,
/// Teclado decimal, con dígitos y separador decimal.
Decimal,
/// Teclado numérico, con sólo dígitos.
Numeric,
/// Teclado de teléfono, con dígitos y símbolos `+`, `*` y `#`.
Tel,
/// Teclado optimizado para búsquedas (puede incluir tecla de búsqueda).
Search,
/// Teclado optimizado para correo electrónico (incluye `@` y `.`).
Email,
/// Teclado optimizado para URL (incluye `/`, `.` y `.com`).
Url,
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Mode::None => "none",
Mode::Text => "text",
Mode::Decimal => "decimal",
Mode::Numeric => "numeric",
Mode::Tel => "tel",
Mode::Search => "search",
Mode::Email => "email",
Mode::Url => "url",
})
}
}
// **< Field >**************************************************************************************
/// Componente para crear un **campo de texto de una línea**.
///
/// Renderiza los tipos más habituales en formularios:
///
/// - [`Field::text()`]: campo de texto genérico (`type="text"`, por defecto).
/// - [`Field::password()`]: contraseña (`type="password"`).
/// - [`Field::search()`]: búsqueda (`type="search"`).
/// - [`Field::email()`]: correo electrónico (`type="email"`).
/// - [`Field::telephone()`]: teléfono (`type="tel"`).
/// - [`Field::url()`]: URL (`type="url"`).
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// let email = form::input::Field::email()
/// .with_name("email")
/// .with_label(L10n::n("Email address"))
/// .with_placeholder(L10n::n("user@example.com"))
/// .with_autocomplete(Some(form::Autocomplete::email()))
/// .with_required(true);
/// ```
///
/// Al enviar el formulario el navegador transmite `name=valor`. Un campo de texto siempre envía su
/// valor, incluso si está vacío. En el servidor se deserializa como `String`:
///
/// ```rust,ignore
/// #[derive(serde::Deserialize)]
/// struct FormData {
/// email: String, // Siempre presente; cadena vacía si el usuario no escribió nada.
/// }
/// ```
#[derive(AutoDefault, Clone, Debug, Getters)]
pub struct Field {
#[getters(skip)]
id: AttrId,
/// Devuelve las clases CSS del contenedor del campo.
classes: Classes,
/// Devuelve el tipo de campo.
kind: Kind,
/// Devuelve el nombre del campo.
name: AttrName,
/// Devuelve el valor inicial del campo.
value: AttrValue,
/// Devuelve la etiqueta del campo.
label: Attr<L10n>,
/// Devuelve el texto de ayuda del campo.
help_text: Attr<L10n>,
/// Devuelve la longitud mínima permitida en caracteres.
minlength: Attr<u16>,
/// Devuelve la longitud máxima permitida en caracteres.
maxlength: Attr<u16>,
/// Devuelve el texto indicativo del campo.
placeholder: Attr<L10n>,
/// Devuelve la configuración de autocompletado del campo.
autocomplete: Attr<form::Autocomplete>,
/// Devuelve si el campo recibe el foco automáticamente al cargar la página.
autofocus: bool,
/// Devuelve si el campo es de sólo lectura.
readonly: bool,
/// Devuelve si el campo es obligatorio.
required: bool,
/// Devuelve si el campo está deshabilitado.
disabled: bool,
/// Devuelve si el campo se muestra como texto plano sin bordes ni fondo.
plaintext: bool,
/// Devuelve la sugerencia de teclado virtual para el campo.
inputmode: Attr<Mode>,
}
impl Component for Field {
fn new() -> Self { fn new() -> Self {
Self::default() Self::default()
} }
fn id(&self) -> Option<String> {
self.id.get()
}
fn setup(&mut self, _cx: &Context) { fn setup(&mut self, _cx: &Context) {
self.alter_classes( self.alter_classes(
ClassesOp::Prepend, ClassesOp::Prepend,
util::join!("form-item form-type-", self.input_type().to_string()), util::join!("form-field form-field-", self.kind().to_string()),
); );
} }
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
let id = self.name().get().map(|name| util::join!("edit-", name)); 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"
} else {
"form-control"
};
Ok(html! { Ok(html! {
div class=[self.classes().get()] { div id=[container_id.as_deref()] class=[self.classes().get()] {
@if let Some(label) = self.label().lookup(cx) { @if let Some(label) = self.label().lookup(cx) {
label for=[&id] class="form-label" { label for=[input_id.as_deref()] class="form-label" {
(label) (label)
@if *self.required() { @if *self.required() {
span span
@ -54,20 +204,20 @@ impl Component for Input {
} }
} }
input input
type=(self.input_type()) type=(self.kind())
id=[id] id=[input_id.as_deref()]
class="form-control" class=(input_class)
name=[self.name().get()] name=[self.name().get()]
value=[self.value().get()] value=[self.value().get()]
size=[self.size().get()]
minlength=[self.minlength().get()] minlength=[self.minlength().get()]
maxlength=[self.maxlength().get()] maxlength=[self.maxlength().get()]
placeholder=[self.placeholder().get()] placeholder=[self.placeholder().lookup(cx)]
inputmode=[self.inputmode().get()]
autocomplete=[self.autocomplete().get()] autocomplete=[self.autocomplete().get()]
autofocus[*self.autofocus()] autofocus[*self.autofocus()]
readonly[*self.readonly()] readonly[*self.readonly() || *self.plaintext()]
required[*self.required()] required[*self.required()]
disabled[*self.disabled()] {} disabled[*self.disabled()];
@if let Some(description) = self.help_text().lookup(cx) { @if let Some(description) = self.help_text().lookup(cx) {
div class="form-text" { (description) } div class="form-text" { (description) }
} }
@ -76,130 +226,197 @@ impl Component for Input {
} }
} }
impl Input { impl Field {
pub fn textfield() -> Self { /// Crea un campo de **texto genérico** (`type="text"`).
Input::default() ///
/// Es el tipo por defecto. Adecuado para nombres, apellidos, ciudades y cualquier entrada
/// textual sin restricciones de formato específicas.
pub fn text() -> Self {
Field::default()
} }
/// Crea un campo de **contraseña** (`type="password"`).
///
/// El navegador oculta los caracteres introducidos. Se recomienda usar con
/// [`with_autocomplete()`](Field::with_autocomplete) para indicar si acepta la contraseña
/// actual o una nueva.
pub fn password() -> Self { pub fn password() -> Self {
Self { Self {
input_type: form::InputType::Password, kind: Kind::Password,
..Default::default() ..Default::default()
} }
} }
/// Crea un campo de **búsqueda** (`type="search"`).
///
/// Semánticamente equivalente a `text` pero optimizado para búsquedas: algunos
/// navegadores añaden un botón para borrar el contenido.
pub fn search() -> Self { pub fn search() -> Self {
Self { Self {
input_type: form::InputType::Search, kind: Kind::Search,
..Default::default() ..Default::default()
} }
} }
/// Crea un campo de **correo electrónico** (`type="email"`).
///
/// El navegador valida el formato de la dirección antes de enviar el formulario. En
/// dispositivos móviles muestra un teclado adaptado para introducir direcciones de correo.
pub fn email() -> Self { pub fn email() -> Self {
Self { Self {
input_type: form::InputType::Email, kind: Kind::Email,
..Default::default() ..Default::default()
} }
} }
/// Crea un campo de **teléfono** (`type="tel"`).
///
/// No impone ninguna restricción de formato (los formatos de teléfono varían por país), pero
/// en dispositivos móviles muestra el teclado numérico de llamadas.
pub fn telephone() -> Self { pub fn telephone() -> Self {
Self { Self {
input_type: form::InputType::Telephone, kind: Kind::Telephone,
..Default::default() ..Default::default()
} }
} }
/// Crea un campo de **URL** (`type="url"`).
///
/// El navegador valida que el valor sea una URL bien formada antes de enviar el formulario.
pub fn url() -> Self { pub fn url() -> Self {
Self { Self {
input_type: form::InputType::Url, kind: Kind::Url,
..Default::default() ..Default::default()
} }
} }
// **< Input BUILDER >************************************************************************** // **< Field BUILDER >**************************************************************************
/// Modifica la lista de clases CSS aplicadas al `input`. /// Establece el identificador único (`id`) del contenedor del campo.
#[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_id(id);
self
}
/// Modifica la lista de clases CSS aplicadas al contenedor del campo.
#[builder_fn] #[builder_fn]
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self { pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
self.classes.alter_classes(op, classes); self.classes.alter_classes(op, classes);
self self
} }
/// Establece el nombre del campo (atributo `name`).
///
/// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
#[builder_fn] #[builder_fn]
pub fn with_name(mut self, name: impl AsRef<str>) -> Self { pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
self.name.alter_name(name); self.name.alter_name(name);
self self
} }
/// Establece el valor inicial del campo.
#[builder_fn] #[builder_fn]
pub fn with_value(mut self, value: impl AsRef<str>) -> Self { pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
self.value.alter_str(value); self.value.alter_str(value);
self self
} }
/// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla).
#[builder_fn] #[builder_fn]
pub fn with_label(mut self, label: L10n) -> Self { pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
self.label.alter_value(label); self.label.alter_opt(label.into());
self self
} }
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
#[builder_fn] #[builder_fn]
pub fn with_help_text(mut self, help_text: L10n) -> Self { pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
self.help_text.alter_value(help_text); self.help_text.alter_opt(help_text.into());
self
}
#[builder_fn]
pub fn with_size(mut self, size: Option<u16>) -> Self {
self.size.alter_opt(size);
self self
} }
/// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo).
#[builder_fn] #[builder_fn]
pub fn with_minlength(mut self, minlength: Option<u16>) -> Self { pub fn with_minlength(mut self, minlength: Option<u16>) -> Self {
self.minlength.alter_opt(minlength); self.minlength.alter_opt(minlength);
self self
} }
/// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite).
#[builder_fn] #[builder_fn]
pub fn with_maxlength(mut self, maxlength: Option<u16>) -> Self { pub fn with_maxlength(mut self, maxlength: Option<u16>) -> Self {
self.maxlength.alter_opt(maxlength); self.maxlength.alter_opt(maxlength);
self self
} }
/// Establece o elimina el texto indicativo del campo (`None` para quitarlo).
///
/// Este texto aparece en el mismo campo y desaparece en cuanto el usuario empieza a escribir.
/// Al ser texto visible para el usuario se acepta [`L10n`] para poder localizarlo.
#[builder_fn] #[builder_fn]
pub fn with_placeholder(mut self, placeholder: impl AsRef<str>) -> Self { pub fn with_placeholder(mut self, placeholder: impl Into<Option<L10n>>) -> Self {
self.placeholder.alter_str(placeholder); self.placeholder.alter_opt(placeholder.into());
self self
} }
/// Establece la configuración de autocompletado del campo.
///
/// Usar los métodos de [`form::Autocomplete`] para los valores más habituales (p. ej.
/// [`Autocomplete::email()`](form::Autocomplete::email) o
/// [`Autocomplete::current_password()`](form::Autocomplete::current_password)).
#[builder_fn] #[builder_fn]
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self { pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self {
self.autocomplete.alter_opt(autocomplete); self.autocomplete.alter_opt(autocomplete);
self self
} }
/// Establece si el campo recibe el foco automáticamente al cargar la página.
#[builder_fn] #[builder_fn]
pub fn with_autofocus(mut self, autofocus: bool) -> Self { pub fn with_autofocus(mut self, autofocus: bool) -> Self {
self.autofocus = autofocus; self.autofocus = autofocus;
self self
} }
/// Establece si el campo es de sólo lectura.
#[builder_fn] #[builder_fn]
pub fn with_readonly(mut self, readonly: bool) -> Self { pub fn with_readonly(mut self, readonly: bool) -> Self {
self.readonly = readonly; self.readonly = readonly;
self self
} }
/// Establece si el campo es obligatorio.
#[builder_fn] #[builder_fn]
pub fn with_required(mut self, required: bool) -> Self { pub fn with_required(mut self, required: bool) -> Self {
self.required = required; self.required = required;
self self
} }
/// Establece si el campo está deshabilitado.
#[builder_fn] #[builder_fn]
pub fn with_disabled(mut self, disabled: bool) -> Self { pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled; self.disabled = disabled;
self self
} }
/// Establece si el campo se muestra como texto plano (sin bordes ni fondo).
///
/// Útil para mostrar un valor no editable en pantalla que sí se envía al servidor con el
/// formulario.
#[builder_fn]
pub fn with_plaintext(mut self, plaintext: bool) -> Self {
self.plaintext = plaintext;
self
}
/// Establece el modo de entrada sugerido para el teclado virtual en dispositivos móviles.
///
/// A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los valores aceptados
/// ni activa la validación del navegador; es sólo una sugerencia de presentación.
#[builder_fn]
pub fn with_inputmode(mut self, inputmode: Option<Mode>) -> Self {
self.inputmode.alter_opt(inputmode);
self
}
} }

View file

@ -21,68 +21,93 @@ pub enum CheckboxKind {
// lo soporta. También se añadiría el constructor `Checkbox::native_switch()`. // lo soporta. También se añadiría el constructor `Checkbox::native_switch()`.
} }
// **< Autocomplete >******************************************************************************* // **< Autocomplete / AutofillField >***************************************************************
/// Valor del atributo HTML `autocomplete`. /// Configuración para el autocompletado de controles en un formulario.
/// ///
/// Según la [especificación](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill) /// Indica al navegador si puede sugerir o rellenar automáticamente el valor del control usando
/// oficial este valor puede ser: /// datos que el usuario haya introducido antes (credenciales guardadas, datos de contacto, etc.).
/// ///
/// - `on` / `off`, o /// Lo habitual es usar uno de los **métodos predefinidos**, que generan el token canónico adecuado
/// - una lista ordenada de tokens predefinidos separados por espacios. /// para cada tipo de dato:
/// ///
/// Las variantes de `Autocomplete` permiten: /// - Identidad y credenciales: [`username()`](Autocomplete::username),
/// [`email()`](Autocomplete::email), [`current_password()`](Autocomplete::current_password),
/// [`new_password()`](Autocomplete::new_password), [`otp()`](Autocomplete::otp).
/// - Token o tokens directos: [`token(field)`](Autocomplete::token) con una variante de
/// [`AutofillField`].
/// - Direcciones: [`shipping(field)`](Autocomplete::shipping),
/// [`billing(field)`](Autocomplete::billing).
/// - Datos de contacto: [`home(field)`](Autocomplete::home), [`work(field)`](Autocomplete::work),
/// [`mobile(field)`](Autocomplete::mobile), [`fax(field)`](Autocomplete::fax),
/// [`pager(field)`](Autocomplete::pager).
/// - Sección personalizada: [`section(name, field)`](Autocomplete::section).
/// ///
/// - Generar valores canónicos `on`/`off` ([`Autocomplete::On`], [`Autocomplete::Off`]). /// Para activar o inhibir el autocompletado sin especificar el tipo de dato basta con usar las
/// - Generar una lista de tokens en formato texto ([`Autocomplete::Custom`]). Los valores creados /// variantes [`form::Autocomplete::On`](Autocomplete::On) o
/// mediante [`Autocomplete::custom()`] se normalizan con [`util::normalize_ascii_or_empty()`]. /// [`form::Autocomplete::Off`](Autocomplete::Off). Para combinaciones no cubiertas por los métodos
/// anteriores, [`custom()`](Autocomplete::custom) acepta cualquier cadena ASCII válida.
/// ///
/// Las entradas no válidas que lleguen a [`Autocomplete::custom()`] se degradan a /// # Ejemplo
/// [`Autocomplete::On`] (valor canónico y seguro). ///
/// ```rust
/// # use pagetop::prelude::*;
/// # use pagetop_bootsier::prelude::*;
/// // Correo electrónico con sugerencia semántica del navegador.
/// let ac = form::Autocomplete::email();
///
/// // Contraseña nueva en un formulario de registro.
/// let ac = form::Autocomplete::new_password();
///
/// // Teléfono de contacto del trabajo.
/// let ac = form::Autocomplete::work(form::AutofillField::Tel);
/// ```
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum Autocomplete { pub enum Autocomplete {
/// Genera `autocomplete="on"`. /// Genera `autocomplete="on"`.
On, On,
/// Genera `autocomplete="off"`. /// Genera `autocomplete="off"`.
Off, Off,
/// Genera un valor personalizado (se espera en formato canónico). /// Contiene el valor literal del atributo `autocomplete` tal como se enviará al navegador.
/// ///
/// Normalmente contiene una lista de tokens separados por espacios (p. ej. `"username"` o /// Debe contener un token o lista de tokens separados por espacios (p. ej. `"username"` o
/// `"username webauthn"`). /// `"username webauthn"`).
Custom(CowStr), Custom(CowStr),
} }
impl Autocomplete { impl Autocomplete {
// --< Field token >---------------------------------------------------------------------------- // --< Token >----------------------------------------------------------------------------------
/// Genera `autocomplete="<field>"` usando un campo predefinido. /// Genera `autocomplete` a partir del token o tokens del [`AutofillField`] indicado.
#[inline] #[inline]
pub fn field(field: AutofillField) -> Self { pub fn token(field: AutofillField) -> Self {
Self::Custom(Cow::Borrowed(field.as_str())) Self::Custom(Cow::Borrowed(field.as_str()))
} }
// --< Sections >------------------------------------------------------------------------------- // --< Secciones >------------------------------------------------------------------------------
/// Construye `autocomplete` usando un nombre de sección y un campo predefinido. /// Construye `autocomplete` con un prefijo de sección y un token o tokens del
/// [`form::AutofillField`](AutofillField) indicado.
/// ///
/// Genera `autocomplete="section-<name> <field>"`. /// Genera `autocomplete="section-<name> <field>"`. Si `name` no es ASCII o contiene espacios,
/// se ignora la sección y se genera sólo el token indicado.
/// ///
/// Si `name` contiene espacios tras normalizar con [`util::normalize_ascii()`] (o si no es /// El prefijo `section-*` sirve para distinguir entre varios grupos del mismo tipo en una misma
/// ASCII / queda vacío), se ignora la sección y se genera solo el campo (`<field>`). /// página (p. ej. una dirección de envío y otra de facturación).
pub fn section(name: impl AsRef<str>, field: AutofillField) -> Self { pub fn section(name: impl AsRef<str>, field: AutofillField) -> Self {
match util::normalize_ascii(name.as_ref()) { match util::normalize_ascii(name.as_ref()) {
Ok(n) if !n.as_ref().contains(' ') => { Ok(n) if !n.as_ref().contains(' ') => {
Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str())) Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str()))
} }
_ => Self::field(field), _ => Self::token(field),
} }
} }
// --< Common fields >-------------------------------------------------------------------------- // --< Comunes >--------------------------------------------------------------------------------
/// Genera `autocomplete="username"`. /// Genera `autocomplete="username"`.
pub fn username() -> Self { pub fn username() -> Self {
Self::field(AutofillField::Username) Self::token(AutofillField::Username)
} }
/// Genera `autocomplete="username webauthn"` (Passkeys / WebAuthn). /// Genera `autocomplete="username webauthn"` (Passkeys / WebAuthn).
@ -92,12 +117,12 @@ impl Autocomplete {
/// Genera `autocomplete="email"`. /// Genera `autocomplete="email"`.
pub fn email() -> Self { pub fn email() -> Self {
Self::field(AutofillField::Email) Self::token(AutofillField::Email)
} }
/// Genera `autocomplete="current-password"`. /// Genera `autocomplete="current-password"`.
pub fn current_password() -> Self { pub fn current_password() -> Self {
Self::field(AutofillField::CurrentPassword) Self::token(AutofillField::CurrentPassword)
} }
/// Genera `autocomplete="current-password webauthn"` (Passkeys / WebAuthn). /// Genera `autocomplete="current-password webauthn"` (Passkeys / WebAuthn).
@ -107,15 +132,15 @@ impl Autocomplete {
/// Genera `autocomplete="new-password"`. /// Genera `autocomplete="new-password"`.
pub fn new_password() -> Self { pub fn new_password() -> Self {
Self::field(AutofillField::NewPassword) Self::token(AutofillField::NewPassword)
} }
/// Genera `autocomplete="one-time-code"`. /// Genera `autocomplete="one-time-code"`.
pub fn otp() -> Self { pub fn otp() -> Self {
Self::field(AutofillField::OneTimeCode) Self::token(AutofillField::OneTimeCode)
} }
// --< Address contexts >----------------------------------------------------------------------- // --< Direcciones >----------------------------------------------------------------------------
/// Contexto de dirección de envío. Genera `autocomplete="shipping <field>"`. /// Contexto de dirección de envío. Genera `autocomplete="shipping <field>"`.
pub fn shipping(field: AutofillField) -> Self { pub fn shipping(field: AutofillField) -> Self {
@ -127,7 +152,7 @@ impl Autocomplete {
Self::Custom(Cow::Owned(util::join!("billing ", field.as_str()))) Self::Custom(Cow::Owned(util::join!("billing ", field.as_str())))
} }
// --< Contact hints >-------------------------------------------------------------------------- // --< Contacto >-------------------------------------------------------------------------------
/// Detalle de contacto: `autocomplete="home <field>"`. /// Detalle de contacto: `autocomplete="home <field>"`.
pub fn home(field: AutofillField) -> Self { pub fn home(field: AutofillField) -> Self {
@ -154,21 +179,17 @@ impl Autocomplete {
Self::Custom(Cow::Owned(util::join!("pager ", field.as_str()))) Self::Custom(Cow::Owned(util::join!("pager ", field.as_str())))
} }
// --< Custom tokens >-------------------------------------------------------------------------- // --< Tokens personalizados >------------------------------------------------------------------
/// Crea un `autocomplete` con texto libre (se espera en formato canónico). /// Crea un valor de `autocomplete` a partir de una cadena de texto libre.
/// ///
/// Esta función acepta una cadena con `on`/`off` o una lista de tokens separados por espacios: /// Normaliza la entrada recortando espacios extra, compactando separadores y convirtiendo a
/// minúsculas. Si el resultado es `"on"` u `"off"`, devuelve la variante correspondiente; si la
/// entrada contiene caracteres no ASCII o queda vacía tras normalizar, devuelve
/// [`form::Autocomplete::On`](Autocomplete::On).
/// ///
/// - Rechaza entradas no ASCII. /// Para los casos habituales se recomienda usar los métodos predefinidos de
/// - Recorta separadores ASCII al inicio/fin. /// [`form::Autocomplete`](Autocomplete).
/// - Compacta secuencias de separadores ASCII en un único espacio.
/// - Convierte a minúsculas.
///
/// - Si el valor normalizado es `"on"` o `"off"`, devuelve [`Autocomplete::On`] o
/// [`Autocomplete::Off`].
/// - Si el valor es inválido (vacío tras normalizar o contiene bytes no ASCII), devuelve
/// [`Autocomplete::On`].
pub fn custom(autocomplete: impl Into<CowStr>) -> Self { pub fn custom(autocomplete: impl Into<CowStr>) -> Self {
let value: CowStr = autocomplete.into(); let value: CowStr = autocomplete.into();
let raw = value.as_ref(); let raw = value.as_ref();
@ -206,9 +227,31 @@ impl fmt::Display for Autocomplete {
} }
} }
/// Tokens para el autocompletado de formularios con [`form::Autocomplete`](Autocomplete).
///
/// Representa los tokens de autorrelleno (*autofill field*) definidos por la
/// [especificación WHATWG](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field)
/// para el atributo `autocomplete`. Cada variante corresponde exactamente a un token canónico
/// de dicha especificación.
///
/// Los valores se usan en combinación con [`form::Autocomplete`](Autocomplete) para construir el
/// valor completo del atributo `autocomplete` de un control de formulario. Los métodos de
/// [`form::Autocomplete`](Autocomplete) como [`token()`](Autocomplete::token),
/// [`email()`](Autocomplete::email), [`shipping()`](Autocomplete::shipping) o
/// [`section()`](Autocomplete::section) aceptan variantes de `AutofillField` para generar el token
/// correspondiente.
///
/// # Ejemplo
///
/// ```rust
/// # use pagetop_bootsier::prelude::*;
/// let ac = form::Autocomplete::token(form::AutofillField::Username);
/// let ac = form::Autocomplete::shipping(form::AutofillField::StreetAddress);
/// let ac = form::Autocomplete::section("job", form::AutofillField::Email);
/// ```
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum AutofillField { pub enum AutofillField {
// Identidad / cuenta // --< Identidad / cuenta >---------------------------------------------------------------------
/// Nombre completo. /// Nombre completo.
Name, Name,
/// Tratamiento o título (p. ej. "Sr.", "Sra.", "Dra."). /// Tratamiento o título (p. ej. "Sr.", "Sra.", "Dra.").
@ -226,7 +269,7 @@ pub enum AutofillField {
/// Identificador de usuario (login). /// Identificador de usuario (login).
Username, Username,
// Credenciales // --< Credenciales >---------------------------------------------------------------------------
/// Contraseña actual. /// Contraseña actual.
CurrentPassword, CurrentPassword,
/// Nueva contraseña. /// Nueva contraseña.
@ -234,13 +277,13 @@ pub enum AutofillField {
/// Código de un solo uso (OTP). /// Código de un solo uso (OTP).
OneTimeCode, OneTimeCode,
// Organización // --< Organización >---------------------------------------------------------------------------
/// Cargo o título dentro de una organización. /// Cargo o título dentro de una organización.
OrganizationTitle, OrganizationTitle,
/// Nombre de la organización. /// Nombre de la organización.
Organization, Organization,
// Contacto // --< Contacto >-------------------------------------------------------------------------------
/// Correo electrónico. /// Correo electrónico.
Email, Email,
/// Teléfono. /// Teléfono.
@ -259,12 +302,12 @@ pub enum AutofillField {
TelLocalSuffix, TelLocalSuffix,
/// Extensión interna. /// Extensión interna.
TelExtension, TelExtension,
/// URL. /// URL personal o de contacto.
Url, Url,
/// Referencia de mensajería instantánea (URL). /// Referencia de mensajería instantánea (URL).
Impp, Impp,
// Dirección (muy habitual en formularios) // --< Dirección >------------------------------------------------------------------------------
/// Dirección postal completa (una sola línea/textarea). /// Dirección postal completa (una sola línea/textarea).
StreetAddress, StreetAddress,
/// Línea 1 de dirección. /// Línea 1 de dirección.
@ -283,12 +326,12 @@ pub enum AutofillField {
AddressLevel1, AddressLevel1,
/// Código postal. /// Código postal.
PostalCode, PostalCode,
/// País (código o token `country`). /// País (el navegador rellena el código de país).
Country, Country,
/// Nombre del país. /// Nombre del país.
CountryName, CountryName,
// Pago (si algún día lo necesitas) // --< Pago >-----------------------------------------------------------------------------------
/// Nombre del titular de la tarjeta. /// Nombre del titular de la tarjeta.
CcName, CcName,
/// Nombre de pila del titular de la tarjeta. /// Nombre de pila del titular de la tarjeta.
@ -310,7 +353,7 @@ pub enum AutofillField {
/// Tipo de tarjeta (p. ej. visa/mastercard). /// Tipo de tarjeta (p. ej. visa/mastercard).
CcType, CcType,
// Transacción / preferencias // --< Transacción / preferencias >-------------------------------------------------------------
/// Moneda preferida para la transacción (código ISO 4217). /// Moneda preferida para la transacción (código ISO 4217).
TransactionCurrency, TransactionCurrency,
/// Cantidad de la transacción (número). /// Cantidad de la transacción (número).
@ -318,7 +361,7 @@ pub enum AutofillField {
/// Idioma preferido (BCP 47). /// Idioma preferido (BCP 47).
Language, Language,
// Otros datos personales (según necesidad del producto) // --< Datos personales >-----------------------------------------------------------------------
/// Fecha de nacimiento completa. /// Fecha de nacimiento completa.
Bday, Bday,
/// Día de nacimiento. /// Día de nacimiento.
@ -327,9 +370,9 @@ pub enum AutofillField {
BdayMonth, BdayMonth,
/// Año de nacimiento. /// Año de nacimiento.
BdayYear, BdayYear,
/// Sexo (según el valor que el UA tenga guardado). /// Sexo (valor libre guardado por el navegador).
Sex, Sex,
/// Foto (URL o referencia, según UA). /// Foto (URL o referencia guardada por el navegador).
Photo, Photo,
} }
@ -402,32 +445,6 @@ impl AutofillField {
} }
} }
// **< InputType >**********************************************************************************
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
pub enum InputType {
#[default]
Textfield,
Password,
Search,
Email,
Telephone,
Url,
}
impl fmt::Display for InputType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
InputType::Textfield => "text",
InputType::Password => "password",
InputType::Search => "search",
InputType::Email => "email",
InputType::Telephone => "tel",
InputType::Url => "url",
})
}
}
// **< Method >************************************************************************************* // **< Method >*************************************************************************************
/// Método HTTP usado por un formulario ([`Form`](crate::theme::Form)) para el envío de los datos. /// Método HTTP usado por un formulario ([`Form`](crate::theme::Form)) para el envío de los datos.

View file

@ -302,7 +302,7 @@ impl Component for Field {
impl Field { impl Field {
// **< Field BUILDER >*************************************************************************** // **< Field BUILDER >***************************************************************************
/// Establece el identificador único (`id`) del control. /// Establece el identificador único (`id`) del contenedor del campo.
#[builder_fn] #[builder_fn]
pub fn with_id(mut self, id: impl AsRef<str>) -> Self { pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
self.id.alter_id(id); self.id.alter_id(id);
@ -400,7 +400,7 @@ impl Field {
self self
} }
/// Establece si el control recibe el foco automáticamente al cargar la página. /// Establece si el campo recibe el foco automáticamente al cargar la página.
#[builder_fn] #[builder_fn]
pub fn with_autofocus(mut self, autofocus: bool) -> Self { pub fn with_autofocus(mut self, autofocus: bool) -> Self {
self.autofocus = autofocus; self.autofocus = autofocus;
@ -414,7 +414,7 @@ impl Field {
self self
} }
/// Establece si el control está deshabilitado. /// Establece si el campo está deshabilitado.
#[builder_fn] #[builder_fn]
pub fn with_disabled(mut self, disabled: bool) -> Self { pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled; self.disabled = disabled;