diff --git a/extensions/pagetop-bootsier/src/theme/form.rs b/extensions/pagetop-bootsier/src/theme/form.rs index 38868a78..85b3fb5d 100644 --- a/extensions/pagetop-bootsier/src/theme/form.rs +++ b/extensions/pagetop-bootsier/src/theme/form.rs @@ -1,8 +1,9 @@ //! Definiciones para crear formularios ([`Form`]). mod props; +pub use props::Method; pub use props::{Autocomplete, AutofillField}; -pub use props::{CheckboxKind, InputType, Method}; +pub use props::CheckboxKind; mod component; pub use component::Form; @@ -10,9 +11,6 @@ pub use component::Form; mod fieldset; pub use fieldset::Fieldset; -mod input; -pub use input::Input; - mod checkbox; pub use checkbox::Checkbox; @@ -21,3 +19,5 @@ pub mod check; pub mod radio; pub mod select; + +pub mod input; diff --git a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs index c7f2f24d..b5d0e822 100644 --- a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs +++ b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs @@ -19,8 +19,8 @@ use pagetop::prelude::*; /// let personal_data = form::Fieldset::new() /// .with_legend(L10n::n("Personal data")) /// .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::email().with_name("email").with_label(L10n::n("Email"))); +/// .with_child(form::input::Field::text().with_name("name").with_label(L10n::n("Full name"))) +/// .with_child(form::input::Field::email().with_name("email").with_label(L10n::n("Email"))); /// ``` #[derive(AutoDefault, Clone, Debug, Getters)] pub struct Fieldset { diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs index 409fc816..7cf4b6fc 100644 --- a/extensions/pagetop-bootsier/src/theme/form/input.rs +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -1,47 +1,197 @@ +//! Definiciones para crear campos de texto de una línea. + use pagetop::prelude::*; use crate::theme::form; use crate::LOCALES_BOOTSIER; -#[derive(AutoDefault, Clone, Debug, Getters)] -pub struct Input { - classes: Classes, - input_type: form::InputType, - name: AttrName, - value: AttrValue, - label: Attr, - help_text: Attr, - #[default(_code = "Attr::::some(60)")] - size: Attr, - minlength: Attr, - #[default(_code = "Attr::::some(128)")] - maxlength: Attr, - placeholder: AttrValue, - autocomplete: Attr, - autofocus: bool, - readonly: bool, - required: bool, - disabled: bool, +use std::fmt; + +// **< Kind >*************************************************************************************** + +/// Tipo de campo para un [`form::input::Field`]. +/// +/// Determina el tipo de entrada que acepta, así como el comportamiento del navegador al interactuar +/// con el campo. Implícitamente se aplica al crear el control: [`text()`](Field::text), +/// [`password()`](Field::password), [`search()`](Field::search), [`email()`](Field::email), +/// [`telephone()`](Field::telephone) o [`url()`](Field::url). +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum Kind { + /// Entrada de texto genérico (`type="text"`). Es el tipo por defecto. + #[default] + Text, + /// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado. + Password, + /// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda. + 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, + /// Devuelve el texto de ayuda del campo. + help_text: Attr, + /// Devuelve la longitud mínima permitida en caracteres. + minlength: Attr, + /// Devuelve la longitud máxima permitida en caracteres. + maxlength: Attr, + /// Devuelve el texto indicativo del campo. + placeholder: Attr, + /// Devuelve la configuración de autocompletado del campo. + autocomplete: Attr, + /// 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, +} + +impl Component for Field { fn new() -> Self { Self::default() } + fn id(&self) -> Option { + self.id.get() + } + fn setup(&mut self, _cx: &Context) { self.alter_classes( 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 { - 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! { - div class=[self.classes().get()] { + div id=[container_id.as_deref()] class=[self.classes().get()] { @if let Some(label) = self.label().lookup(cx) { - label for=[&id] class="form-label" { + label for=[input_id.as_deref()] class="form-label" { (label) @if *self.required() { span @@ -54,20 +204,20 @@ impl Component for Input { } } input - type=(self.input_type()) - id=[id] - class="form-control" + type=(self.kind()) + id=[input_id.as_deref()] + class=(input_class) name=[self.name().get()] value=[self.value().get()] - size=[self.size().get()] minlength=[self.minlength().get()] maxlength=[self.maxlength().get()] - placeholder=[self.placeholder().get()] + placeholder=[self.placeholder().lookup(cx)] + inputmode=[self.inputmode().get()] autocomplete=[self.autocomplete().get()] autofocus[*self.autofocus()] - readonly[*self.readonly()] + readonly[*self.readonly() || *self.plaintext()] required[*self.required()] - disabled[*self.disabled()] {} + disabled[*self.disabled()]; @if let Some(description) = self.help_text().lookup(cx) { div class="form-text" { (description) } } @@ -76,130 +226,197 @@ impl Component for Input { } } -impl Input { - pub fn textfield() -> Self { - Input::default() +impl Field { + /// Crea un campo de **texto genérico** (`type="text"`). + /// + /// 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 { Self { - input_type: form::InputType::Password, + kind: Kind::Password, ..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 { Self { - input_type: form::InputType::Search, + kind: Kind::Search, ..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 { Self { - input_type: form::InputType::Email, + kind: Kind::Email, ..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 { Self { - input_type: form::InputType::Telephone, + kind: Kind::Telephone, ..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 { Self { - input_type: form::InputType::Url, + kind: Kind::Url, ..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) -> Self { + self.id.alter_id(id); + self + } + + /// Modifica la lista de clases CSS aplicadas al contenedor del campo. #[builder_fn] pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { self.classes.alter_classes(op, classes); 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] pub fn with_name(mut self, name: impl AsRef) -> Self { self.name.alter_name(name); self } + /// Establece el valor inicial del campo. #[builder_fn] pub fn with_value(mut self, value: impl AsRef) -> Self { self.value.alter_str(value); self } + /// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla). #[builder_fn] - pub fn with_label(mut self, label: L10n) -> Self { - self.label.alter_value(label); + pub fn with_label(mut self, label: impl Into>) -> Self { + self.label.alter_opt(label.into()); self } + /// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo). #[builder_fn] - pub fn with_help_text(mut self, help_text: L10n) -> Self { - self.help_text.alter_value(help_text); - self - } - - #[builder_fn] - pub fn with_size(mut self, size: Option) -> Self { - self.size.alter_opt(size); + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); self } + /// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo). #[builder_fn] pub fn with_minlength(mut self, minlength: Option) -> Self { self.minlength.alter_opt(minlength); self } + /// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite). #[builder_fn] pub fn with_maxlength(mut self, maxlength: Option) -> Self { self.maxlength.alter_opt(maxlength); 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] - pub fn with_placeholder(mut self, placeholder: impl AsRef) -> Self { - self.placeholder.alter_str(placeholder); + pub fn with_placeholder(mut self, placeholder: impl Into>) -> Self { + self.placeholder.alter_opt(placeholder.into()); 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] pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { self.autocomplete.alter_opt(autocomplete); self } + /// Establece si el campo recibe el foco automáticamente al cargar la página. #[builder_fn] pub fn with_autofocus(mut self, autofocus: bool) -> Self { self.autofocus = autofocus; self } + /// Establece si el campo es de sólo lectura. #[builder_fn] pub fn with_readonly(mut self, readonly: bool) -> Self { self.readonly = readonly; self } + /// Establece si el campo es obligatorio. #[builder_fn] pub fn with_required(mut self, required: bool) -> Self { self.required = required; self } + /// Establece si el campo está deshabilitado. #[builder_fn] pub fn with_disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; 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) -> Self { + self.inputmode.alter_opt(inputmode); + self + } } diff --git a/extensions/pagetop-bootsier/src/theme/form/props.rs b/extensions/pagetop-bootsier/src/theme/form/props.rs index 24593045..dbd1e705 100644 --- a/extensions/pagetop-bootsier/src/theme/form/props.rs +++ b/extensions/pagetop-bootsier/src/theme/form/props.rs @@ -21,68 +21,93 @@ pub enum CheckboxKind { // 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) -/// oficial este valor puede ser: +/// Indica al navegador si puede sugerir o rellenar automáticamente el valor del control usando +/// datos que el usuario haya introducido antes (credenciales guardadas, datos de contacto, etc.). /// -/// - `on` / `off`, o -/// - una lista ordenada de tokens predefinidos separados por espacios. +/// Lo habitual es usar uno de los **métodos predefinidos**, que generan el token canónico adecuado +/// 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`]). -/// - Generar una lista de tokens en formato texto ([`Autocomplete::Custom`]). Los valores creados -/// mediante [`Autocomplete::custom()`] se normalizan con [`util::normalize_ascii_or_empty()`]. +/// Para activar o inhibir el autocompletado sin especificar el tipo de dato basta con usar las +/// variantes [`form::Autocomplete::On`](Autocomplete::On) o +/// [`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 -/// [`Autocomplete::On`] (valor canónico y seguro). +/// # Ejemplo +/// +/// ```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)] pub enum Autocomplete { /// Genera `autocomplete="on"`. On, /// Genera `autocomplete="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"`). Custom(CowStr), } impl Autocomplete { - // --< Field token >---------------------------------------------------------------------------- + // --< Token >---------------------------------------------------------------------------------- - /// Genera `autocomplete=""` usando un campo predefinido. + /// Genera `autocomplete` a partir del token o tokens del [`AutofillField`] indicado. #[inline] - pub fn field(field: AutofillField) -> Self { + pub fn token(field: AutofillField) -> Self { 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- "`. + /// Genera `autocomplete="section- "`. 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 - /// ASCII / queda vacío), se ignora la sección y se genera solo el campo (``). + /// El prefijo `section-*` sirve para distinguir entre varios grupos del mismo tipo en una misma + /// página (p. ej. una dirección de envío y otra de facturación). pub fn section(name: impl AsRef, field: AutofillField) -> Self { match util::normalize_ascii(name.as_ref()) { Ok(n) if !n.as_ref().contains(' ') => { Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str())) } - _ => Self::field(field), + _ => Self::token(field), } } - // --< Common fields >-------------------------------------------------------------------------- + // --< Comunes >-------------------------------------------------------------------------------- /// Genera `autocomplete="username"`. pub fn username() -> Self { - Self::field(AutofillField::Username) + Self::token(AutofillField::Username) } /// Genera `autocomplete="username webauthn"` (Passkeys / WebAuthn). @@ -92,12 +117,12 @@ impl Autocomplete { /// Genera `autocomplete="email"`. pub fn email() -> Self { - Self::field(AutofillField::Email) + Self::token(AutofillField::Email) } /// Genera `autocomplete="current-password"`. pub fn current_password() -> Self { - Self::field(AutofillField::CurrentPassword) + Self::token(AutofillField::CurrentPassword) } /// Genera `autocomplete="current-password webauthn"` (Passkeys / WebAuthn). @@ -107,15 +132,15 @@ impl Autocomplete { /// Genera `autocomplete="new-password"`. pub fn new_password() -> Self { - Self::field(AutofillField::NewPassword) + Self::token(AutofillField::NewPassword) } /// Genera `autocomplete="one-time-code"`. 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 "`. pub fn shipping(field: AutofillField) -> Self { @@ -127,7 +152,7 @@ impl Autocomplete { Self::Custom(Cow::Owned(util::join!("billing ", field.as_str()))) } - // --< Contact hints >-------------------------------------------------------------------------- + // --< Contacto >------------------------------------------------------------------------------- /// Detalle de contacto: `autocomplete="home "`. pub fn home(field: AutofillField) -> Self { @@ -154,21 +179,17 @@ impl Autocomplete { 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. - /// - Recorta separadores ASCII al inicio/fin. - /// - 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`]. + /// Para los casos habituales se recomienda usar los métodos predefinidos de + /// [`form::Autocomplete`](Autocomplete). pub fn custom(autocomplete: impl Into) -> Self { let value: CowStr = autocomplete.into(); 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)] pub enum AutofillField { - // Identidad / cuenta + // --< Identidad / cuenta >--------------------------------------------------------------------- /// Nombre completo. Name, /// Tratamiento o título (p. ej. "Sr.", "Sra.", "Dra."). @@ -226,7 +269,7 @@ pub enum AutofillField { /// Identificador de usuario (login). Username, - // Credenciales + // --< Credenciales >--------------------------------------------------------------------------- /// Contraseña actual. CurrentPassword, /// Nueva contraseña. @@ -234,13 +277,13 @@ pub enum AutofillField { /// Código de un solo uso (OTP). OneTimeCode, - // Organización + // --< Organización >--------------------------------------------------------------------------- /// Cargo o título dentro de una organización. OrganizationTitle, /// Nombre de la organización. Organization, - // Contacto + // --< Contacto >------------------------------------------------------------------------------- /// Correo electrónico. Email, /// Teléfono. @@ -259,12 +302,12 @@ pub enum AutofillField { TelLocalSuffix, /// Extensión interna. TelExtension, - /// URL. + /// URL personal o de contacto. Url, /// Referencia de mensajería instantánea (URL). Impp, - // Dirección (muy habitual en formularios) + // --< Dirección >------------------------------------------------------------------------------ /// Dirección postal completa (una sola línea/textarea). StreetAddress, /// Línea 1 de dirección. @@ -283,12 +326,12 @@ pub enum AutofillField { AddressLevel1, /// Código postal. PostalCode, - /// País (código o token `country`). + /// País (el navegador rellena el código de país). Country, /// Nombre del país. CountryName, - // Pago (si algún día lo necesitas) + // --< Pago >----------------------------------------------------------------------------------- /// Nombre del titular de la tarjeta. CcName, /// Nombre de pila del titular de la tarjeta. @@ -310,7 +353,7 @@ pub enum AutofillField { /// Tipo de tarjeta (p. ej. visa/mastercard). CcType, - // Transacción / preferencias + // --< Transacción / preferencias >------------------------------------------------------------- /// Moneda preferida para la transacción (código ISO 4217). TransactionCurrency, /// Cantidad de la transacción (número). @@ -318,7 +361,7 @@ pub enum AutofillField { /// Idioma preferido (BCP 47). Language, - // Otros datos personales (según necesidad del producto) + // --< Datos personales >----------------------------------------------------------------------- /// Fecha de nacimiento completa. Bday, /// Día de nacimiento. @@ -327,9 +370,9 @@ pub enum AutofillField { BdayMonth, /// Año de nacimiento. BdayYear, - /// Sexo (según el valor que el UA tenga guardado). + /// Sexo (valor libre guardado por el navegador). Sex, - /// Foto (URL o referencia, según UA). + /// Foto (URL o referencia guardada por el navegador). 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 >************************************************************************************* /// Método HTTP usado por un formulario ([`Form`](crate::theme::Form)) para el envío de los datos. diff --git a/extensions/pagetop-bootsier/src/theme/form/select.rs b/extensions/pagetop-bootsier/src/theme/form/select.rs index 10c6e9d8..7fef083c 100644 --- a/extensions/pagetop-bootsier/src/theme/form/select.rs +++ b/extensions/pagetop-bootsier/src/theme/form/select.rs @@ -302,7 +302,7 @@ impl Component for Field { impl Field { // **< Field BUILDER >*************************************************************************** - /// Establece el identificador único (`id`) del control. + /// Establece el identificador único (`id`) del contenedor del campo. #[builder_fn] pub fn with_id(mut self, id: impl AsRef) -> Self { self.id.alter_id(id); @@ -400,7 +400,7 @@ impl Field { 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] pub fn with_autofocus(mut self, autofocus: bool) -> Self { self.autofocus = autofocus; @@ -414,7 +414,7 @@ impl Field { self } - /// Establece si el control está deshabilitado. + /// Establece si el campo está deshabilitado. #[builder_fn] pub fn with_disabled(mut self, disabled: bool) -> Self { self.disabled = disabled;