From 7803062a185743d5492035bbddf3dfded8de41b9 Mon Sep 17 00:00:00 2001 From: Manuel Cillero Date: Fri, 1 May 2026 12:32:10 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(bootsier):=20A=C3=B1ade=20componen?= =?UTF-8?q?te=20para=20=C3=A1reas=20de=20texto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extensions/pagetop-bootsier/src/theme/form.rs | 3 + .../src/theme/form/textarea.rs | 290 ++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 extensions/pagetop-bootsier/src/theme/form/textarea.rs diff --git a/extensions/pagetop-bootsier/src/theme/form.rs b/extensions/pagetop-bootsier/src/theme/form.rs index 85b3fb5d..03304843 100644 --- a/extensions/pagetop-bootsier/src/theme/form.rs +++ b/extensions/pagetop-bootsier/src/theme/form.rs @@ -21,3 +21,6 @@ pub mod radio; pub mod select; pub mod input; + +mod textarea; +pub use textarea::Textarea; diff --git a/extensions/pagetop-bootsier/src/theme/form/textarea.rs b/extensions/pagetop-bootsier/src/theme/form/textarea.rs new file mode 100644 index 00000000..c545df8c --- /dev/null +++ b/extensions/pagetop-bootsier/src/theme/form/textarea.rs @@ -0,0 +1,290 @@ +use pagetop::prelude::*; + +use crate::theme::form; +use crate::LOCALES_BOOTSIER; + +/// Componente para crear un **área de texto** de formulario. +/// +/// Permite escribir en un área de texto de más de una línea, con una etiqueta opcional y atributos +/// como el número de filas a presentar, longitud mínima (`minlength`) y máxima (`maxlength`), texto +/// indicativo (`placeholder`) o autocompletado (`autocomplete`). +/// +/// # Ejemplo +/// +/// ```rust +/// # use pagetop::prelude::*; +/// # use pagetop_bootsier::prelude::*; +/// let descripcion = form::Textarea::new() +/// .with_name("description") +/// .with_label(L10n::n("Description")) +/// .with_rows(Some(8)) +/// .with_maxlength(Some(500)) +/// .with_placeholder(L10n::n("Write here...")) +/// .with_required(true); +/// ``` +/// +/// Al enviar el formulario el navegador transmite `name=valor`. Un área de texto siempre envía su +/// valor, incluso si está vacía. En el servidor se deserializa como `String`: +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct FormData { +/// description: String, // Siempre presente; cadena vacía si el usuario no escribió nada. +/// } +/// ``` +#[derive(AutoDefault, Clone, Debug, Getters)] +pub struct Textarea { + #[getters(skip)] + id: AttrId, + /// Devuelve las clases CSS del contenedor del área de texto. + classes: Classes, + /// Devuelve el nombre del campo. + name: AttrName, + /// Devuelve el valor inicial del área de texto. + value: AttrValue, + /// Devuelve la etiqueta del campo. + label: Attr, + /// Devuelve si la etiqueta se muestra flotante sobre el campo. + floating_label: bool, + /// Devuelve el texto de ayuda del campo. + help_text: Attr, + /// Devuelve el número de filas visibles del área de texto. + rows: 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 área de texto. + 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, +} + +impl Component for Textarea { + fn new() -> Self { + Self::default() + } + + fn id(&self) -> Option { + self.id.get() + } + + fn setup(&mut self, _cx: &Context) { + if *self.floating_label() { + self.rows.alter_opt(None::); + self.alter_classes(ClassesOp::Prepend, "form-floating"); + } + self.alter_classes(ClassesOp::Prepend, "form-field form-field-textarea"); + } + + fn prepare(&self, cx: &mut Context) -> Result { + let container_id = self + .id() + .or_else(|| self.name().get().map(|n| util::join!("edit-", n))); + let textarea_id = container_id + .as_deref() + .map(|id| util::join!(id, "-textarea")); + // La etiqueta flotante requiere el atributo `placeholder` para detectar cuándo el campo + // está vacío y animar la etiqueta; si no está definido, se fuerza `placeholder=""`. + 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=[textarea_id.as_deref()] class="form-label" { + (text) + @if *self.required() { + span + class="form-required" + title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx)) + { + "*" + } + } + } + }, + None => html! {}, + }; + Ok(html! { + div id=[container_id.as_deref()] class=[self.classes().get()] { + @if !*self.floating_label() { + (label) + } + textarea + id=[textarea_id.as_deref()] + class="form-control" + name=[self.name().get()] + rows=[self.rows().get()] + minlength=[self.minlength().get()] + maxlength=[self.maxlength().get()] + placeholder=[placeholder] + autocomplete=[self.autocomplete().get()] + autofocus[*self.autofocus()] + readonly[*self.readonly()] + required[*self.required()] + disabled[*self.disabled()] + { + @if let Some(value) = self.value().get() { + (value) + } + } + @if *self.floating_label() { + (label) + } + @if let Some(description) = self.help_text().lookup(cx) { + div class="form-text" { (description) } + } + } + }) + } +} + +impl Textarea { + // **< Textarea BUILDER >*********************************************************************** + + /// 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 área de texto. + #[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: impl Into>) -> Self { + self.label.alter_opt(label.into()); + self + } + + /// Establece si la etiqueta se muestra flotante sobre el campo. + /// + /// Cuando está activo, la etiqueta se superpone al área de texto y asciende al enfocarlo o + /// cuando tiene contenido. + /// + /// Si se usa la etiqueta flotante, el [`setup()`](Self::setup) del componente anulará el valor + /// establecido con [`with_rows()`](Self::with_rows) antes del renderizado. Si es necesario, se + /// puede controlar la altura con estilos aplicados al componente. + #[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). + #[builder_fn] + pub fn with_help_text(mut self, help_text: impl Into>) -> Self { + self.help_text.alter_opt(help_text.into()); + self + } + + /// Establece el número de filas visibles del área de texto. + /// + /// Sin valor o pasando `None`, el área muestra su altura predeterminada, dos filas según el + /// estándar. + /// + /// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label). + #[builder_fn] + pub fn with_rows(mut self, rows: Option) -> Self { + self.rows.alter_opt(rows); + self + } + + /// Establece la longitud mínima permitida en caracteres. + #[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. + #[builder_fn] + pub fn with_maxlength(mut self, maxlength: Option) -> Self { + self.maxlength.alter_opt(maxlength); + self + } + + /// Establece o elimina el texto indicativo del área de texto (`None` para quitarlo). + /// + /// Este texto aparece en el área de texto 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 Into>) -> Self { + self.placeholder.alter_opt(placeholder.into()); + self + } + + /// Establece la configuración de autocompletado del campo. + /// + /// Permite al navegador sugerir o rellenar automáticamente el contenido del área de texto + /// con valores guardados. Es especialmente útil en áreas con contenido semántico predefinido. + /// + /// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para + /// omitir el atributo. + #[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 + } +}