✨ (bootsier): Añade etiquetas flotantes
Los componentes `input::Field` y `select::Field` admiten ahora `with_floating_label()` para habilitar etiquetas flotantes en la visualización de los componentes.
This commit is contained in:
parent
bf1d942223
commit
ef6b1fd75e
2 changed files with 117 additions and 50 deletions
|
|
@ -95,12 +95,12 @@ impl fmt::Display for Mode {
|
||||||
///
|
///
|
||||||
/// Renderiza los tipos más habituales en formularios:
|
/// Renderiza los tipos más habituales en formularios:
|
||||||
///
|
///
|
||||||
/// - [`Field::text()`]: campo de texto genérico (`type="text"`, por defecto).
|
/// - [`form::input::Field::text()`]: campo de texto genérico (`type="text"`, por defecto).
|
||||||
/// - [`Field::password()`]: contraseña (`type="password"`).
|
/// - [`form::input::Field::password()`]: contraseña (`type="password"`).
|
||||||
/// - [`Field::search()`]: búsqueda (`type="search"`).
|
/// - [`form::input::Field::search()`]: búsqueda (`type="search"`).
|
||||||
/// - [`Field::email()`]: correo electrónico (`type="email"`).
|
/// - [`form::input::Field::email()`]: correo electrónico (`type="email"`).
|
||||||
/// - [`Field::telephone()`]: teléfono (`type="tel"`).
|
/// - [`form::input::Field::telephone()`]: teléfono (`type="tel"`).
|
||||||
/// - [`Field::url()`]: URL (`type="url"`).
|
/// - [`form::input::Field::url()`]: URL (`type="url"`).
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -138,6 +138,8 @@ pub struct Field {
|
||||||
value: AttrValue,
|
value: AttrValue,
|
||||||
/// Devuelve la etiqueta del campo.
|
/// Devuelve la etiqueta del campo.
|
||||||
label: Attr<L10n>,
|
label: Attr<L10n>,
|
||||||
|
/// Devuelve si la etiqueta se muestra flotante sobre el campo.
|
||||||
|
floating_label: bool,
|
||||||
/// Devuelve el texto de ayuda del campo.
|
/// Devuelve el texto de ayuda del campo.
|
||||||
help_text: Attr<L10n>,
|
help_text: Attr<L10n>,
|
||||||
/// Devuelve la longitud mínima permitida en caracteres.
|
/// Devuelve la longitud mínima permitida en caracteres.
|
||||||
|
|
@ -172,6 +174,9 @@ impl Component for Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
|
if *self.floating_label() {
|
||||||
|
self.alter_classes(ClassesOp::Prepend, "form-floating");
|
||||||
|
}
|
||||||
self.alter_classes(
|
self.alter_classes(
|
||||||
ClassesOp::Prepend,
|
ClassesOp::Prepend,
|
||||||
util::join!("form-field form-field-", self.kind().to_string()),
|
util::join!("form-field form-field-", self.kind().to_string()),
|
||||||
|
|
@ -188,11 +193,17 @@ impl Component for Field {
|
||||||
} else {
|
} else {
|
||||||
"form-control"
|
"form-control"
|
||||||
};
|
};
|
||||||
Ok(html! {
|
// La etiqueta flotante requiere el atributo `placeholder` para detectar cuándo el campo
|
||||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
// está vacío y animar la etiqueta; si no está definido, se fuerza `placeholder=""`.
|
||||||
@if let Some(label) = self.label().lookup(cx) {
|
let placeholder = if *self.floating_label() {
|
||||||
|
Some(self.placeholder().lookup(cx).unwrap_or_default())
|
||||||
|
} else {
|
||||||
|
self.placeholder().lookup(cx)
|
||||||
|
};
|
||||||
|
let label = match self.label().lookup(cx) {
|
||||||
|
Some(text) => html! {
|
||||||
label for=[input_id.as_deref()] class="form-label" {
|
label for=[input_id.as_deref()] class="form-label" {
|
||||||
(label)
|
(text)
|
||||||
@if *self.required() {
|
@if *self.required() {
|
||||||
span
|
span
|
||||||
class="form-required"
|
class="form-required"
|
||||||
|
|
@ -202,6 +213,13 @@ impl Component for Field {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
None => html! {},
|
||||||
|
};
|
||||||
|
Ok(html! {
|
||||||
|
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||||
|
@if !*self.floating_label() {
|
||||||
|
(label)
|
||||||
}
|
}
|
||||||
input
|
input
|
||||||
type=(self.kind())
|
type=(self.kind())
|
||||||
|
|
@ -211,13 +229,16 @@ impl Component for Field {
|
||||||
value=[self.value().get()]
|
value=[self.value().get()]
|
||||||
minlength=[self.minlength().get()]
|
minlength=[self.minlength().get()]
|
||||||
maxlength=[self.maxlength().get()]
|
maxlength=[self.maxlength().get()]
|
||||||
placeholder=[self.placeholder().lookup(cx)]
|
placeholder=[placeholder]
|
||||||
inputmode=[self.inputmode().get()]
|
inputmode=[self.inputmode().get()]
|
||||||
autocomplete=[self.autocomplete().get()]
|
autocomplete=[self.autocomplete().get()]
|
||||||
autofocus[*self.autofocus()]
|
autofocus[*self.autofocus()]
|
||||||
readonly[*self.readonly() || *self.plaintext()]
|
readonly[*self.readonly() || *self.plaintext()]
|
||||||
required[*self.required()]
|
required[*self.required()]
|
||||||
disabled[*self.disabled()];
|
disabled[*self.disabled()];
|
||||||
|
@if *self.floating_label() {
|
||||||
|
(label)
|
||||||
|
}
|
||||||
@if let Some(description) = self.help_text().lookup(cx) {
|
@if let Some(description) = self.help_text().lookup(cx) {
|
||||||
div class="form-text" { (description) }
|
div class="form-text" { (description) }
|
||||||
}
|
}
|
||||||
|
|
@ -232,14 +253,14 @@ impl Field {
|
||||||
/// Es el tipo por defecto. Adecuado para nombres, apellidos, ciudades y cualquier entrada
|
/// Es el tipo por defecto. Adecuado para nombres, apellidos, ciudades y cualquier entrada
|
||||||
/// textual sin restricciones de formato específicas.
|
/// textual sin restricciones de formato específicas.
|
||||||
pub fn text() -> Self {
|
pub fn text() -> Self {
|
||||||
Field::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un campo de **contraseña** (`type="password"`).
|
/// Crea un campo de **contraseña** (`type="password"`).
|
||||||
///
|
///
|
||||||
/// El navegador oculta los caracteres introducidos. Se recomienda usar con
|
/// El navegador oculta los caracteres introducidos. Se recomienda usar con
|
||||||
/// [`with_autocomplete()`](Field::with_autocomplete) para indicar si acepta la contraseña
|
/// [`with_autocomplete()`](Self::with_autocomplete) para permitir autorrellenar con una
|
||||||
/// actual o una nueva.
|
/// contraseña guardada o dejar al usuario recibir sugerencias o crear una nueva.
|
||||||
pub fn password() -> Self {
|
pub fn password() -> Self {
|
||||||
Self {
|
Self {
|
||||||
kind: Kind::Password,
|
kind: Kind::Password,
|
||||||
|
|
@ -330,6 +351,16 @@ impl Field {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Establece si la etiqueta se muestra flotante sobre el campo.
|
||||||
|
///
|
||||||
|
/// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene
|
||||||
|
/// contenido.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_floating_label(mut self, floating_label: bool) -> Self {
|
||||||
|
self.floating_label = floating_label;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ use crate::LOCALES_BOOTSIER;
|
||||||
/// Cada elemento tiene un valor que se envía al servidor y una etiqueta localizable visible para el
|
/// Cada elemento tiene un valor que se envía al servidor y una etiqueta localizable visible para el
|
||||||
/// usuario.
|
/// usuario.
|
||||||
///
|
///
|
||||||
/// Puede marcarse como seleccionado por defecto con [`with_selected()`](Item::with_selected) o
|
/// Puede marcarse como seleccionado por defecto con [`with_selected()`](Self::with_selected) o
|
||||||
/// deshabilitado de forma independiente al resto usando [`with_disabled()`](Item::with_disabled).
|
/// deshabilitado de forma independiente al resto usando [`with_disabled()`](Self::with_disabled).
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -70,7 +70,7 @@ impl Item {
|
||||||
/// Grupo de elementos dentro de [`form::select::Field`].
|
/// Grupo de elementos dentro de [`form::select::Field`].
|
||||||
///
|
///
|
||||||
/// Agrupa un conjunto de elementos dentro de una lista de selección con una etiqueta visible. El
|
/// Agrupa un conjunto de elementos dentro de una lista de selección con una etiqueta visible. El
|
||||||
/// grupo completo puede deshabilitarse en bloque con [`with_disabled()`](Group::with_disabled).
|
/// grupo completo puede deshabilitarse en bloque con [`with_disabled()`](Self::with_disabled).
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -139,10 +139,10 @@ pub enum Entry {
|
||||||
///
|
///
|
||||||
/// Renderiza un campo para mostrar una lista de elementos con una etiqueta opcional. Permite elegir
|
/// Renderiza un campo para mostrar una lista de elementos con una etiqueta opcional. Permite elegir
|
||||||
/// uno, o más de uno si se activa la selección múltiple con
|
/// uno, o más de uno si se activa la selección múltiple con
|
||||||
/// [`with_multiple()`](Field::with_multiple).
|
/// [`with_multiple()`](Self::with_multiple).
|
||||||
///
|
///
|
||||||
/// Los elementos individuales se añaden con [`with_item()`](Field::with_item); los grupos de
|
/// Los elementos individuales se añaden con [`with_item()`](Self::with_item); los grupos de
|
||||||
/// elementos con un encabezado común se añaden con [`with_group()`](Field::with_group). Ambos
|
/// elementos con un encabezado común se añaden con [`with_group()`](Self::with_group). Ambos
|
||||||
/// métodos pueden combinarse libremente.
|
/// métodos pueden combinarse libremente.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
|
|
@ -199,14 +199,16 @@ pub struct Field {
|
||||||
name: AttrName,
|
name: AttrName,
|
||||||
/// Devuelve la etiqueta del campo.
|
/// Devuelve la etiqueta del campo.
|
||||||
label: Attr<L10n>,
|
label: Attr<L10n>,
|
||||||
|
/// Devuelve si la etiqueta se muestra flotante sobre el campo.
|
||||||
|
floating_label: bool,
|
||||||
/// Devuelve el texto de ayuda del campo.
|
/// Devuelve el texto de ayuda del campo.
|
||||||
help_text: Attr<L10n>,
|
help_text: Attr<L10n>,
|
||||||
/// Devuelve las entradas de la lista (elementos individuales y grupos de elementos).
|
/// Devuelve las entradas de la lista (elementos individuales y grupos de elementos).
|
||||||
entries: Vec<Entry>,
|
entries: Vec<Entry>,
|
||||||
/// Devuelve si la lista permite selección múltiple.
|
/// Devuelve si la lista permite selección múltiple.
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
/// Devuelve el número de filas visibles (relevante con selección múltiple o en modo lista).
|
/// Devuelve el número de filas visibles de la lista de selección.
|
||||||
size: Attr<u16>,
|
rows: Attr<u16>,
|
||||||
/// Devuelve la configuración de autocompletado del campo.
|
/// Devuelve la configuración de autocompletado del campo.
|
||||||
autocomplete: Attr<form::Autocomplete>,
|
autocomplete: Attr<form::Autocomplete>,
|
||||||
/// Devuelve si la lista recibe el foco automáticamente al cargar la página.
|
/// Devuelve si la lista recibe el foco automáticamente al cargar la página.
|
||||||
|
|
@ -227,6 +229,11 @@ impl Component for Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
fn setup(&mut self, _cx: &Context) {
|
||||||
|
if *self.floating_label() {
|
||||||
|
self.multiple = false;
|
||||||
|
self.rows.alter_opt(None::<u16>);
|
||||||
|
self.alter_classes(ClassesOp::Prepend, "form-floating");
|
||||||
|
}
|
||||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-select");
|
self.alter_classes(ClassesOp::Prepend, "form-field form-field-select");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,11 +242,10 @@ impl Component for Field {
|
||||||
.id()
|
.id()
|
||||||
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
||||||
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));
|
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));
|
||||||
Ok(html! {
|
let label = match self.label().lookup(cx) {
|
||||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
Some(text) => html! {
|
||||||
@if let Some(label) = self.label().lookup(cx) {
|
|
||||||
label for=[select_id.as_deref()] class="form-label" {
|
label for=[select_id.as_deref()] class="form-label" {
|
||||||
(label)
|
(text)
|
||||||
@if *self.required() {
|
@if *self.required() {
|
||||||
span
|
span
|
||||||
class="form-required"
|
class="form-required"
|
||||||
|
|
@ -249,13 +255,20 @@ impl Component for Field {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
None => html! {},
|
||||||
|
};
|
||||||
|
Ok(html! {
|
||||||
|
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||||
|
@if !*self.floating_label() {
|
||||||
|
(label)
|
||||||
}
|
}
|
||||||
select
|
select
|
||||||
id=[select_id.as_deref()]
|
id=[select_id.as_deref()]
|
||||||
class="form-select"
|
class="form-select"
|
||||||
name=[self.name().get()]
|
name=[self.name().get()]
|
||||||
multiple[*self.multiple()]
|
multiple[*self.multiple()]
|
||||||
size=[self.size().get()]
|
size=[self.rows().get()]
|
||||||
autocomplete=[self.autocomplete().get()]
|
autocomplete=[self.autocomplete().get()]
|
||||||
autofocus[*self.autofocus()]
|
autofocus[*self.autofocus()]
|
||||||
required[*self.required()]
|
required[*self.required()]
|
||||||
|
|
@ -291,6 +304,9 @@ impl Component for Field {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@if *self.floating_label() {
|
||||||
|
(label)
|
||||||
|
}
|
||||||
@if let Some(description) = self.help_text().lookup(cx) {
|
@if let Some(description) = self.help_text().lookup(cx) {
|
||||||
div class="form-text" { (description) }
|
div class="form-text" { (description) }
|
||||||
}
|
}
|
||||||
|
|
@ -333,6 +349,20 @@ impl Field {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Establece si la etiqueta se muestra flotante sobre el campo.
|
||||||
|
///
|
||||||
|
/// Cuando está activo, la etiqueta se superpone al control y permanece flotante siempre que
|
||||||
|
/// haya una opción visible.
|
||||||
|
///
|
||||||
|
/// Si se usa la etiqueta flotante, el [`setup()`](Self::setup) del componente anulará los
|
||||||
|
/// valores establecidos con [`with_multiple()`](Self::with_multiple) y
|
||||||
|
/// [`with_rows()`](Self::with_rows) antes del renderizado.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_floating_label(mut self, floating_label: bool) -> Self {
|
||||||
|
self.floating_label = floating_label;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||||
|
|
@ -361,27 +391,33 @@ impl Field {
|
||||||
/// Establece si el control permite seleccionar varios elementos.
|
/// Establece si el control permite seleccionar varios elementos.
|
||||||
///
|
///
|
||||||
/// Al activar la selección múltiple, se muestra una lista en lugar de un desplegable. Se
|
/// Al activar la selección múltiple, se muestra una lista en lugar de un desplegable. Se
|
||||||
/// recomienda combinar con [`with_size()`](Field::with_size) para controlar el número de filas
|
/// recomienda combinar con [`with_rows()`](Self::with_rows) para controlar el número de filas
|
||||||
/// visibles.
|
/// visibles.
|
||||||
///
|
///
|
||||||
/// Para un número reducido de elementos con etiquetas descriptivas considera usar
|
/// Para un número reducido de elementos con etiquetas descriptivas considera usar
|
||||||
/// [`form::check::Field`] en su lugar, ofrece una presentación más clara y es más accesible en
|
/// [`form::check::Field`] en su lugar, ofrece una presentación más clara y es más accesible en
|
||||||
/// pantallas pequeñas.
|
/// pantallas pequeñas.
|
||||||
|
///
|
||||||
|
/// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label).
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_multiple(mut self, multiple: bool) -> Self {
|
pub fn with_multiple(mut self, multiple: bool) -> Self {
|
||||||
self.multiple = multiple;
|
self.multiple = multiple;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el número de filas visibles en la lista de selección.
|
/// Establece el número de filas visibles de la lista de selección.
|
||||||
///
|
///
|
||||||
/// Cuando se establece un valor mayor que 1, el control se muestra como lista en lugar de
|
/// Cuando se establece un valor mayor que 1, el control se muestra como lista en lugar de
|
||||||
/// desplegable, tanto en modo simple como múltiple. Es especialmente útil con selección
|
/// desplegable, tanto en modo simple como múltiple. Con `None` se omite el atributo y presenta
|
||||||
/// múltiple para controlar el número de filas visibles sin necesidad de recurrir al
|
/// el control como desplegable (comportamiento por defecto).
|
||||||
/// desplazamiento.
|
///
|
||||||
|
/// Es especialmente útil con selección múltiple para controlar el número de filas visibles sin
|
||||||
|
/// necesidad de recurrir al desplazamiento.
|
||||||
|
///
|
||||||
|
/// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label).
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_size(mut self, size: Option<u16>) -> Self {
|
pub fn with_rows(mut self, rows: Option<u16>) -> Self {
|
||||||
self.size.alter_opt(size);
|
self.rows.alter_opt(rows);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue