From bfaf2e569f2fbb12f713cbe3624d9434ec3ca1a2 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Tue, 6 Jan 2026 01:17:35 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(bootsier):=20A=C3=B1ade=20componen?= =?UTF-8?q?tes=20para=20formularios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/locale/en-US/components.ftl | 7 +- .../src/locale/es-ES/components.ftl | 7 +- extensions/pagetop-bootsier/src/theme.rs | 5 + extensions/pagetop-bootsier/src/theme/form.rs | 14 + .../src/theme/form/component.rs | 130 +++++ .../src/theme/form/fieldset.rs | 81 ++++ .../pagetop-bootsier/src/theme/form/input.rs | 205 ++++++++ .../pagetop-bootsier/src/theme/form/props.rs | 449 ++++++++++++++++++ src/html/attr.rs | 7 + 9 files changed, 901 insertions(+), 4 deletions(-) create mode 100644 extensions/pagetop-bootsier/src/theme/form.rs create mode 100644 extensions/pagetop-bootsier/src/theme/form/component.rs create mode 100644 extensions/pagetop-bootsier/src/theme/form/fieldset.rs create mode 100644 extensions/pagetop-bootsier/src/theme/form/input.rs create mode 100644 extensions/pagetop-bootsier/src/theme/form/props.rs diff --git a/extensions/pagetop-bootsier/src/locale/en-US/components.ftl b/extensions/pagetop-bootsier/src/locale/en-US/components.ftl index e3b0d6e6..c73478bf 100644 --- a/extensions/pagetop-bootsier/src/locale/en-US/components.ftl +++ b/extensions/pagetop-bootsier/src/locale/en-US/components.ftl @@ -1,8 +1,11 @@ # Dropdown dropdown_toggle = Toggle Dropdown -# Offcanvas -offcanvas_close = Close +# form::Input +input_required = This field is required # Navbar toggle = Toggle navigation + +# Offcanvas +offcanvas_close = Close diff --git a/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl b/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl index ab7ff687..21b52c91 100644 --- a/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl +++ b/extensions/pagetop-bootsier/src/locale/es-ES/components.ftl @@ -1,8 +1,11 @@ # Dropdown dropdown_toggle = Mostrar/ocultar menú -# Offcanvas -offcanvas_close = Cerrar +# form::Input +input_required = Este campo es obligatorio # Navbar toggle = Mostrar/ocultar navegación + +# Offcanvas +offcanvas_close = Cerrar diff --git a/extensions/pagetop-bootsier/src/theme.rs b/extensions/pagetop-bootsier/src/theme.rs index 2c6b5757..7464caf5 100644 --- a/extensions/pagetop-bootsier/src/theme.rs +++ b/extensions/pagetop-bootsier/src/theme.rs @@ -19,6 +19,11 @@ pub mod dropdown; #[doc(inline)] pub use dropdown::Dropdown; +// Form. +pub mod form; +#[doc(inline)] +pub use form::Form; + // Image. pub mod image; #[doc(inline)] diff --git a/extensions/pagetop-bootsier/src/theme/form.rs b/extensions/pagetop-bootsier/src/theme/form.rs new file mode 100644 index 00000000..ef2a6c32 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/form.rs @@ -0,0 +1,14 @@ +//! Definiciones para crear formularios ([`Form`]). + +mod props; +pub use props::{Autocomplete, AutofillField}; +pub use props::{InputType, Method}; + +mod component; +pub use component::Form; + +mod fieldset; +pub use fieldset::Fieldset; + +mod input; +pub use input::Input; diff --git a/extensions/pagetop-bootsier/src/theme/form/component.rs b/extensions/pagetop-bootsier/src/theme/form/component.rs new file mode 100644 index 00000000..2da46e3f --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/form/component.rs @@ -0,0 +1,130 @@ +use pagetop::prelude::*; + +use crate::theme::form; + +/// Componente para crear un **formulario**. +/// +/// Este componente renderiza un `
` estándar con soporte para los atributos más habituales: +/// +/// - `id`: identificador opcional del formulario. +/// - `classes`: clases CSS adicionales (p. ej. utilidades CSS). +/// - `action`: URL/ruta de destino para el envío. +/// - `method`: método usado por el formulario para el envío de los datos (ver explicaciones en +/// [`form::Method`](crate::theme::form::Method)). +/// - `accept-charset`: juego de caracteres aceptado (por defecto es `"UTF-8"`). +/// - `children`: contenido del formulario. +/// +/// # Ejemplo +/// +/// ```ignore +/// use pagetop::prelude::*; +/// use crate::prelude::*; +/// +/// let form = Form::new() +/// .with_id("search") +/// .with_action("/search") +/// .with_method(form::Method::Get) +/// .with_classes(ClassesOp::Add, "mb-3") +/// .add_child(Input::new().with_name("q")); +/// ``` +#[derive(AutoDefault, Getters)] +pub struct Form { + #[getters(skip)] + id: AttrId, + classes: Classes, + action: AttrValue, + method: form::Method, + #[default(_code = "AttrValue::new(\"UTF-8\")")] + charset: AttrValue, + children: Children, +} + +impl Component for Form { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.id.get() + } + + fn setup_before_prepare(&mut self, _cx: &mut Context) { + self.alter_classes(ClassesOp::Prepend, "form"); + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + let method = match self.method() { + form::Method::Post => Some("post"), + form::Method::Get => None, + }; + PrepareMarkup::With(html! { + form + id=[self.id()] + class=[self.classes().get()] + action=[self.action().get()] + method=[method] + accept-charset=[self.charset().get()] + { + (self.children().render(cx)) + } + }) + } +} + +impl Form { + // **< Form BUILDER >*************************************************************************** + + /// Establece el identificador único (`id`) del formulario. + #[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 formulario. + #[builder_fn] + pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { + self.classes.alter_classes(op, classes); + self + } + + /// Establece la URL/ruta de destino del formulario. + #[builder_fn] + pub fn with_action(mut self, action: impl AsRef) -> Self { + self.action.alter_str(action); + self + } + + /// Establece el método para enviar el formulario. + /// + /// - `GET`: el atributo `method` se omite. + /// - `POST`: se establece `method="post"`. + #[builder_fn] + pub fn with_method(mut self, method: form::Method) -> Self { + self.method = method; + self + } + + /// Establece el juego de caracteres aceptado por el formulario. + /// + /// Por defecto se usa `"UTF-8"`. + #[builder_fn] + pub fn with_charset(mut self, charset: impl AsRef) -> Self { + self.charset.alter_str(charset); + self + } + + /// Añade un nuevo componente hijo al formulario. + #[inline] + pub fn add_child(mut self, component: impl Component) -> Self { + self.children.add(Child::with(component)); + self + } + + /// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`]. + #[builder_fn] + pub fn with_child(mut self, op: ChildOp) -> Self { + self.children.alter_child(op); + self + } +} diff --git a/extensions/pagetop-bootsier/src/theme/form/fieldset.rs b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs new file mode 100644 index 00000000..36092ca2 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/form/fieldset.rs @@ -0,0 +1,81 @@ +use pagetop::prelude::*; + +/// Agrupa controles relacionados de un formulario (`
`). +/// +/// Se usa para mejorar la accesibilidad cuando se acompaña de una leyenda que encabeza el grupo. +#[derive(AutoDefault, Getters)] +pub struct Fieldset { + #[getters(skip)] + id: AttrId, + classes: Classes, + legend: Attr, + disabled: bool, + children: Children, +} + +impl Component for Fieldset { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.id.get() + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + PrepareMarkup::With(html! { + fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] { + @if let Some(legend) = self.legend().lookup(cx) { + legend { (legend) } + } + (self.children().render(cx)) + } + }) + } +} + +impl Fieldset { + // **< Fieldset BUILDER >*********************************************************************** + + /// Establece el identificador único (`id`) del `fieldset` (grupo de controles). + #[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 `fieldset`. + #[builder_fn] + pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { + self.classes.alter_classes(op, classes); + self + } + + /// Establece la leyenda del `fieldset`. + #[builder_fn] + pub fn with_legend(mut self, legend: L10n) -> Self { + self.legend.alter_value(legend); + self + } + + /// Establece si el `fieldset` está deshabilitado. + #[builder_fn] + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } + + /// Añade un nuevo componente hijo al `fieldset`. + #[inline] + pub fn add_child(mut self, component: impl Component) -> Self { + self.children.add(Child::with(component)); + self + } + + /// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`]. + #[builder_fn] + pub fn with_child(mut self, op: ChildOp) -> Self { + self.children.alter_child(op); + self + } +} diff --git a/extensions/pagetop-bootsier/src/theme/form/input.rs b/extensions/pagetop-bootsier/src/theme/form/input.rs new file mode 100644 index 00000000..bf76e082 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/form/input.rs @@ -0,0 +1,205 @@ +use pagetop::prelude::*; + +use crate::theme::form; +use crate::LOCALES_BOOTSIER; + +#[derive(AutoDefault, 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, +} + +impl Component for Input { + fn new() -> Self { + Self::default() + } + + fn setup_before_prepare(&mut self, _cx: &mut Context) { + self.alter_classes( + ClassesOp::Prepend, + util::join!("form-item form-type-", self.input_type().to_string()), + ); + } + + fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup { + let id = self.name().get().map(|name| util::join!("edit-", name)); + PrepareMarkup::With(html! { + div class=[self.classes().get()] { + @if let Some(label) = self.label().lookup(cx) { + label for=[&id] class="form-label" { + (label) + @if *self.required() { + span + class="form-required" + title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx)) + { + "*" + } + } + } + } + input + type=(self.input_type()) + id=[id] + class="form-control" + name=[self.name().get()] + value=[self.value().get()] + size=[self.size().get()] + minlength=[self.minlength().get()] + maxlength=[self.maxlength().get()] + placeholder=[self.placeholder().get()] + autocomplete=[self.autocomplete().get()] + autofocus[*self.autofocus()] + readonly[*self.readonly()] + required[*self.required()] + disabled[*self.disabled()] {} + @if let Some(description) = self.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) + } +} + +impl Input { + pub fn textfield() -> Self { + Input::default() + } + + pub fn password() -> Self { + Self { + input_type: form::InputType::Password, + ..Default::default() + } + } + + pub fn search() -> Self { + Self { + input_type: form::InputType::Search, + ..Default::default() + } + } + + pub fn email() -> Self { + Self { + input_type: form::InputType::Email, + ..Default::default() + } + } + + pub fn telephone() -> Self { + Self { + input_type: form::InputType::Telephone, + ..Default::default() + } + } + + pub fn url() -> Self { + Self { + input_type: form::InputType::Url, + ..Default::default() + } + } + + // **< Input BUILDER >************************************************************************** + + /// Modifica la lista de clases CSS aplicadas al `input`. + #[builder_fn] + pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef) -> Self { + self.classes.alter_classes(op, classes); + self + } + + #[builder_fn] + pub fn with_name(mut self, name: impl AsRef) -> Self { + self.name.alter_name(name); + self + } + + #[builder_fn] + pub fn with_value(mut self, value: impl AsRef) -> Self { + self.value.alter_str(value); + self + } + + #[builder_fn] + pub fn with_label(mut self, label: L10n) -> Self { + self.label.alter_value(label); + self + } + + #[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); + self + } + + #[builder_fn] + pub fn with_minlength(mut self, minlength: Option) -> Self { + self.minlength.alter_opt(minlength); + self + } + + #[builder_fn] + pub fn with_maxlength(mut self, maxlength: Option) -> Self { + self.maxlength.alter_opt(maxlength); + self + } + + #[builder_fn] + pub fn with_placeholder(mut self, placeholder: impl AsRef) -> Self { + self.placeholder.alter_str(placeholder); + self + } + + #[builder_fn] + pub fn with_autocomplete(mut self, autocomplete: Option) -> Self { + self.autocomplete.alter_opt(autocomplete); + self + } + + #[builder_fn] + pub fn with_autofocus(mut self, autofocus: bool) -> Self { + self.autofocus = autofocus; + self + } + + #[builder_fn] + pub fn with_readonly(mut self, readonly: bool) -> Self { + self.readonly = readonly; + self + } + + #[builder_fn] + pub fn with_required(mut self, required: bool) -> Self { + self.required = required; + self + } + + #[builder_fn] + pub fn with_disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} diff --git a/extensions/pagetop-bootsier/src/theme/form/props.rs b/extensions/pagetop-bootsier/src/theme/form/props.rs new file mode 100644 index 00000000..7fedb036 --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/form/props.rs @@ -0,0 +1,449 @@ +use pagetop::prelude::*; + +use std::borrow::Cow; +use std::fmt; + +// **< Autocomplete >******************************************************************************* + +/// Valor del atributo HTML `autocomplete`. +/// +/// Según la [especificación](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill) +/// oficial este valor puede ser: +/// +/// - `on` / `off`, o +/// - una lista ordenada de tokens predefinidos separados por espacios. +/// +/// Las variantes de `Autocomplete` permiten: +/// +/// - 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()`]. +/// +/// Las entradas no válidas que lleguen a [`Autocomplete::custom()`] se degradan a +/// [`Autocomplete::On`] (valor canónico y seguro). +#[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). + /// + /// Normalmente contiene una lista de tokens separados por espacios (p. ej. `"username"` o + /// `"username webauthn"`). + Custom(CowStr), +} + +impl Autocomplete { + // --< Field token >---------------------------------------------------------------------------- + + /// Genera `autocomplete=""` usando un campo predefinido. + #[inline] + pub fn field(field: AutofillField) -> Self { + Self::Custom(Cow::Borrowed(field.as_str())) + } + + // --< Sections >------------------------------------------------------------------------------- + + /// Construye `autocomplete` usando un nombre de sección y un campo predefinido. + /// + /// Genera `autocomplete="section- "`. + /// + /// 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 (``). + 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), + } + } + + // --< Common fields >-------------------------------------------------------------------------- + + /// Genera `autocomplete="username"`. + pub fn username() -> Self { + Self::field(AutofillField::Username) + } + + /// Genera `autocomplete="username webauthn"` (Passkeys / WebAuthn). + pub fn username_webauthn() -> Self { + Self::custom("username webauthn") + } + + /// Genera `autocomplete="email"`. + pub fn email() -> Self { + Self::field(AutofillField::Email) + } + + /// Genera `autocomplete="current-password"`. + pub fn current_password() -> Self { + Self::field(AutofillField::CurrentPassword) + } + + /// Genera `autocomplete="current-password webauthn"` (Passkeys / WebAuthn). + pub fn current_password_webauthn() -> Self { + Self::custom("current-password webauthn") + } + + /// Genera `autocomplete="new-password"`. + pub fn new_password() -> Self { + Self::field(AutofillField::NewPassword) + } + + /// Genera `autocomplete="one-time-code"`. + pub fn otp() -> Self { + Self::field(AutofillField::OneTimeCode) + } + + // --< Address contexts >----------------------------------------------------------------------- + + /// Contexto de dirección de envío. Genera `autocomplete="shipping "`. + pub fn shipping(field: AutofillField) -> Self { + Self::Custom(Cow::Owned(util::join!("shipping ", field.as_str()))) + } + + /// Contexto de dirección de facturación. Genera `autocomplete="billing "`. + pub fn billing(field: AutofillField) -> Self { + Self::Custom(Cow::Owned(util::join!("billing ", field.as_str()))) + } + + // --< Contact hints >-------------------------------------------------------------------------- + + /// Detalle de contacto: `autocomplete="home "`. + pub fn home(field: AutofillField) -> Self { + Self::Custom(Cow::Owned(util::join!("home ", field.as_str()))) + } + + /// Detalle de contacto: `autocomplete="work "`. + pub fn work(field: AutofillField) -> Self { + Self::Custom(Cow::Owned(util::join!("work ", field.as_str()))) + } + + /// Detalle de contacto: `autocomplete="mobile "`. + pub fn mobile(field: AutofillField) -> Self { + Self::Custom(Cow::Owned(util::join!("mobile ", field.as_str()))) + } + + /// Detalle de contacto: `autocomplete="fax "`. + pub fn fax(field: AutofillField) -> Self { + Self::Custom(Cow::Owned(util::join!("fax ", field.as_str()))) + } + + /// Detalle de contacto: `autocomplete="pager "`. + pub fn pager(field: AutofillField) -> Self { + Self::Custom(Cow::Owned(util::join!("pager ", field.as_str()))) + } + + // --< Custom tokens >-------------------------------------------------------------------------- + + /// Crea un `autocomplete` con texto libre (se espera en formato canónico). + /// + /// Esta función acepta una cadena con `on`/`off` o una lista de tokens separados por espacios: + /// + /// - 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`]. + pub fn custom(autocomplete: impl Into) -> Self { + let value: CowStr = autocomplete.into(); + let raw = value.as_ref(); + + // Normaliza la entrada. + let Some(normalized) = util::normalize_ascii_or_empty(raw, "Autocomplete::custom") else { + return Self::On; + }; + let autocomplete = normalized.as_ref(); + + // Identifica valores especiales. + if autocomplete == "on" { + return Self::On; + } + if autocomplete == "off" { + return Self::Off; + } + + // Mantiene el `Cow` original si no cambia nada (no reserva espacio). + if autocomplete == raw { + return Self::Custom(value); + } + // En otro caso asigna espacio para la normalización. + Self::Custom(Cow::Owned(normalized.into_owned())) + } +} + +impl fmt::Display for Autocomplete { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Autocomplete::On => f.write_str("on"), + Autocomplete::Off => f.write_str("off"), + Autocomplete::Custom(c) => f.write_str(c), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AutofillField { + // Identidad / cuenta + /// Nombre completo. + Name, + /// Tratamiento o título (p. ej. "Sr.", "Sra.", "Dra."). + HonorificPrefix, + /// Nombre de pila. + GivenName, + /// Nombre adicional (p. ej. segundo nombre). + AdditionalName, + /// Apellidos. + FamilyName, + /// Sufijo honorífico (p. ej. "Jr.", "PhD"). + HonorificSuffix, + /// Apodo. + Nickname, + /// Identificador de usuario (login). + Username, + + // Credenciales + /// Contraseña actual. + CurrentPassword, + /// Nueva contraseña. + NewPassword, + /// Código de un solo uso (OTP). + OneTimeCode, + + // Organización + /// Cargo o título dentro de una organización. + OrganizationTitle, + /// Nombre de la organización. + Organization, + + // Contacto + /// Correo electrónico. + Email, + /// Teléfono. + Tel, + /// Prefijo/código de país del teléfono (incluye `+`). + TelCountryCode, + /// Teléfono sin el código de país. + TelNational, + /// Código de área (si aplica). + TelAreaCode, + /// Teléfono sin código de país ni de área. + TelLocal, + /// Prefijo local (primera parte tras el área). + TelLocalPrefix, + /// Sufijo local (segunda parte tras el área). + TelLocalSuffix, + /// Extensión interna. + TelExtension, + /// URL. + Url, + /// Referencia de mensajería instantánea (URL). + Impp, + + // Dirección (muy habitual en formularios) + /// Dirección postal completa (una sola línea/textarea). + StreetAddress, + /// Línea 1 de dirección. + AddressLine1, + /// Línea 2 de dirección. + AddressLine2, + /// Línea 3 de dirección. + AddressLine3, + /// Nivel administrativo 4 (el más específico). + AddressLevel4, + /// Nivel administrativo 3. + AddressLevel3, + /// Nivel administrativo 2 (p. ej. ciudad/municipio). + AddressLevel2, + /// Nivel administrativo 1 (p. ej. provincia/estado). + AddressLevel1, + /// Código postal. + PostalCode, + /// País (código o token `country`). + Country, + /// Nombre del país. + CountryName, + + // Pago (si algún día lo necesitas) + /// Nombre del titular de la tarjeta. + CcName, + /// Nombre de pila del titular de la tarjeta. + CcGivenName, + /// Nombre adicional del titular de la tarjeta. + CcAdditionalName, + /// Apellidos del titular de la tarjeta. + CcFamilyName, + /// Número de tarjeta. + CcNumber, + /// Fecha de caducidad (completa). + CcExp, + /// Mes de caducidad. + CcExpMonth, + /// Año de caducidad. + CcExpYear, + /// Código de seguridad (CVC/CVV). + CcCsc, + /// Tipo de tarjeta (p. ej. visa/mastercard). + CcType, + + // Transacción / preferencias + /// Moneda preferida para la transacción (código ISO 4217). + TransactionCurrency, + /// Cantidad de la transacción (número). + TransactionAmount, + /// Idioma preferido (BCP 47). + Language, + + // Otros datos personales (según necesidad del producto) + /// Fecha de nacimiento completa. + Bday, + /// Día de nacimiento. + BdayDay, + /// Mes de nacimiento. + BdayMonth, + /// Año de nacimiento. + BdayYear, + /// Sexo (según el valor que el UA tenga guardado). + Sex, + /// Foto (URL o referencia, según UA). + Photo, +} + +impl AutofillField { + /// Devuelve el token exacto definido por HTML para `autocomplete`. + pub(crate) fn as_str(&self) -> &'static str { + match self { + AutofillField::Name => "name", + AutofillField::HonorificPrefix => "honorific-prefix", + AutofillField::GivenName => "given-name", + AutofillField::AdditionalName => "additional-name", + AutofillField::FamilyName => "family-name", + AutofillField::HonorificSuffix => "honorific-suffix", + AutofillField::Nickname => "nickname", + AutofillField::Username => "username", + + AutofillField::CurrentPassword => "current-password", + AutofillField::NewPassword => "new-password", + AutofillField::OneTimeCode => "one-time-code", + + AutofillField::OrganizationTitle => "organization-title", + AutofillField::Organization => "organization", + + AutofillField::Email => "email", + AutofillField::Tel => "tel", + AutofillField::TelCountryCode => "tel-country-code", + AutofillField::TelNational => "tel-national", + AutofillField::TelAreaCode => "tel-area-code", + AutofillField::TelLocal => "tel-local", + AutofillField::TelLocalPrefix => "tel-local-prefix", + AutofillField::TelLocalSuffix => "tel-local-suffix", + AutofillField::TelExtension => "tel-extension", + AutofillField::Url => "url", + AutofillField::Impp => "impp", + + AutofillField::StreetAddress => "street-address", + AutofillField::AddressLine1 => "address-line1", + AutofillField::AddressLine2 => "address-line2", + AutofillField::AddressLine3 => "address-line3", + AutofillField::AddressLevel4 => "address-level4", + AutofillField::AddressLevel3 => "address-level3", + AutofillField::AddressLevel2 => "address-level2", + AutofillField::AddressLevel1 => "address-level1", + AutofillField::PostalCode => "postal-code", + AutofillField::Country => "country", + AutofillField::CountryName => "country-name", + + AutofillField::CcName => "cc-name", + AutofillField::CcGivenName => "cc-given-name", + AutofillField::CcAdditionalName => "cc-additional-name", + AutofillField::CcFamilyName => "cc-family-name", + AutofillField::CcNumber => "cc-number", + AutofillField::CcExp => "cc-exp", + AutofillField::CcExpMonth => "cc-exp-month", + AutofillField::CcExpYear => "cc-exp-year", + AutofillField::CcCsc => "cc-csc", + AutofillField::CcType => "cc-type", + + AutofillField::TransactionCurrency => "transaction-currency", + AutofillField::TransactionAmount => "transaction-amount", + AutofillField::Language => "language", + + AutofillField::Bday => "bday", + AutofillField::BdayDay => "bday-day", + AutofillField::BdayMonth => "bday-month", + AutofillField::BdayYear => "bday-year", + AutofillField::Sex => "sex", + AutofillField::Photo => "photo", + } + } +} + +// **< 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. +/// +/// En HTML, el atributo `method` del formulario indica **cómo** se envían los datos: +/// +/// - **GET**: los pares `name=value` se codifican en la **URL** añadiendo una cadena de consulta +/// como `?a=1&b=2`. Es el método por defecto en HTML cuando no se especifica. Suele ser apropiado +/// para **búsquedas** o formularios que no modifican datos ni el estado del sistema. +/// +/// - **POST**: los datos se envían en el **cuerpo** de la petición (*request body*). Es apropiado +/// para acciones que **modifican el estado** o cuando hay formularios grandes. Es el **método por +/// defecto** en PageTop. +/// +/// # Consideraciones prácticas +/// +/// - **Visibilidad y privacidad**: con GET los datos quedan visibles en la URL (historial, *logs*, +/// marcadores). No se recomienda para datos sensibles. Con POST no van en la URL, pero **no se +/// cifran** por sí mismos; por eso es esencial el uso de HTTPS. +/// - **Tamaño**: GET está limitado por la longitud máxima de URL que acepten el navegador y el +/// servidor. POST es más flexible para cargas grandes. +/// - **Ficheros**: la subida de ficheros requiere `method="post"` y un `enctype` adecuado +/// (habitualmente `multipart/form-data`). +#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] +pub enum Method { + /// Envía los datos en el cuerpo de la petición. + /// + /// Es el **método por defecto** en PageTop. Recomendado para operaciones que modifican el + /// estado o para envíos grandes. + #[default] + Post, + + /// Envía los datos en la URL como una cadena *query*. + /// + /// Recomendado para búsquedas y operaciones que no modifican datos ni el estado del sistema. + Get, +} diff --git a/src/html/attr.rs b/src/html/attr.rs index 01c78280..61f7252c 100644 --- a/src/html/attr.rs +++ b/src/html/attr.rs @@ -25,6 +25,13 @@ impl Attr { // **< Attr BUILDER >************************************************************************ + /// Establece un valor opcional para el atributo. + #[builder_fn] + pub fn with_opt(mut self, opt: Option) -> Self { + self.0 = opt; + self + } + /// Establece un valor para el atributo. #[builder_fn] pub fn with_value(mut self, value: T) -> Self {