✨ (bootsier): Añade campos de texto de una línea
This commit is contained in:
parent
9273be2110
commit
f85f2ea2d1
5 changed files with 377 additions and 143 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<L10n>,
|
||||
help_text: Attr<L10n>,
|
||||
#[default(_code = "Attr::<u16>::some(60)")]
|
||||
size: Attr<u16>,
|
||||
minlength: Attr<u16>,
|
||||
#[default(_code = "Attr::<u16>::some(128)")]
|
||||
maxlength: Attr<u16>,
|
||||
placeholder: AttrValue,
|
||||
autocomplete: Attr<form::Autocomplete>,
|
||||
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<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 {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
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<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! {
|
||||
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<str>) -> 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<str>) -> 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<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor inicial del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> 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<Option<L10n>>) -> 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<u16>) -> Self {
|
||||
self.size.alter_opt(size);
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> 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<u16>) -> 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<u16>) -> 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<str>) -> Self {
|
||||
self.placeholder.alter_str(placeholder);
|
||||
pub fn with_placeholder(mut self, placeholder: impl Into<Option<L10n>>) -> 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<form::Autocomplete>) -> 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<Mode>) -> Self {
|
||||
self.inputmode.alter_opt(inputmode);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="<field>"` 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-<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
|
||||
/// ASCII / queda vacío), se ignora la sección y se genera solo el campo (`<field>`).
|
||||
/// 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<str>, 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 <field>"`.
|
||||
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 <field>"`.
|
||||
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<CowStr>) -> 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.
|
||||
|
|
|
|||
|
|
@ -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<str>) -> 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue