Compare commits
No commits in common. "1cab0ae9a08cb600e6813702f959652147d072c3" and "3e83f8e35e7581398d73225d02d4756840d1a46e" have entirely different histories.
1cab0ae9a0
...
3e83f8e35e
23 changed files with 238 additions and 535 deletions
|
|
@ -102,7 +102,7 @@ impl Extension for SuperMenu {
|
||||||
)),
|
)),
|
||||||
));
|
));
|
||||||
|
|
||||||
InRegion::Global(&CoreRegions::Header).add(
|
InRegion::Global(&CoreRegion::Header).add(
|
||||||
bs::Container::new()
|
bs::Container::new()
|
||||||
.with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
.with_width(bs::container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
||||||
.with_child(navbar_menu),
|
.with_child(navbar_menu),
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,38 @@
|
||||||
//! Definiciones y plantillas del tema Bootsier.
|
//! Definiciones y componentes del tema.
|
||||||
|
//!
|
||||||
|
//! En esta página, el apartado **Modules** incluye las definiciones necesarias para los componentes
|
||||||
|
//! que se muestran en el apartado **Structs**, mientras que en **Enums** se listan los elementos
|
||||||
|
//! auxiliares del tema utilizados en clases y componentes.
|
||||||
|
|
||||||
pub mod bs;
|
mod attrs;
|
||||||
|
pub use attrs::*;
|
||||||
|
|
||||||
pub mod class;
|
pub mod classes;
|
||||||
|
|
||||||
mod token;
|
// Button.
|
||||||
pub use token::*;
|
mod button;
|
||||||
|
pub use button::{Button, ButtonAction};
|
||||||
|
|
||||||
|
// Container.
|
||||||
|
pub mod container;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use container::Container;
|
||||||
|
|
||||||
|
// Dropdown.
|
||||||
|
pub mod dropdown;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use dropdown::Dropdown;
|
||||||
|
|
||||||
|
// Form.
|
||||||
|
pub mod form;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use form::Form;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use bs::badge::BadgeBootsier;
|
pub use form::input::InputBootsier;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use bs::container::ContainerBootsier;
|
pub use form::select::SelectBootsier;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use bs::form::input::InputBootsier;
|
pub use form::textarea::TextareaBootsier;
|
||||||
#[doc(hidden)]
|
|
||||||
pub use bs::form::select::SelectBootsier;
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use bs::form::textarea::TextareaBootsier;
|
|
||||||
|
|
||||||
// Image.
|
// Image.
|
||||||
pub mod image;
|
pub mod image;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
//! Componentes proporcionados por el tema.
|
//! Componentes proporcionados por el tema.
|
||||||
|
|
||||||
// Badge.
|
|
||||||
pub(crate) mod badge;
|
|
||||||
pub use badge::{Badge, BadgeBootsier};
|
|
||||||
|
|
||||||
// Button.
|
// Button.
|
||||||
mod button;
|
mod button;
|
||||||
pub use button::{Button, ButtonAction};
|
pub use button::{Button, ButtonAction};
|
||||||
|
|
@ -24,12 +20,6 @@ pub use dropdown::Dropdown;
|
||||||
pub mod form;
|
pub mod form;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use form::Form;
|
pub use form::Form;
|
||||||
#[doc(inline)]
|
|
||||||
pub use form::input::InputBootsier;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use form::select::SelectBootsier;
|
|
||||||
#[doc(inline)]
|
|
||||||
pub use form::textarea::TextareaBootsier;
|
|
||||||
|
|
||||||
// Image.
|
// Image.
|
||||||
pub mod image;
|
pub mod image;
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::*;
|
|
||||||
|
|
||||||
pub use pagetop::base::component::Badge;
|
|
||||||
|
|
||||||
const EXTRA_TEXT_BG: &str = "bootsier.badge.text_bg";
|
|
||||||
|
|
||||||
/// Extensión de Bootsier para [`Badge`].
|
|
||||||
///
|
|
||||||
/// Proporciona el método [`with_text_bg()`](Self::with_text_bg) para fijar el color de fondo del
|
|
||||||
/// badge usando un color de texto con contraste suficiente garantizado.
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
/// use pagetop_bootsier::theme::*;
|
|
||||||
///
|
|
||||||
/// let admin = bs::Badge::new()
|
|
||||||
/// .with_label(L10n::n("Admin"))
|
|
||||||
/// .with_text_bg(ThemeColor::Danger);
|
|
||||||
/// ```
|
|
||||||
pub trait BadgeBootsier {
|
|
||||||
#[builder_fn]
|
|
||||||
fn with_text_bg(self, color: ThemeColor) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BadgeBootsier for Badge {
|
|
||||||
/// Establece el color de fondo usando un color de texto con contraste garantizado.
|
|
||||||
///
|
|
||||||
/// Igual a `with_prop(PropsOp::add_classes(class::TextColor::Bg(color)))`, pero sin el riesgo
|
|
||||||
/// de acumular más de una clase `text-bg-{color}` si se llama varias veces.
|
|
||||||
#[builder_fn]
|
|
||||||
fn with_text_bg(mut self, color: ThemeColor) -> Self {
|
|
||||||
self.alter_prop(PropsOp::set_extra(EXTRA_TEXT_BG, color));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// **< Badge SETUP >********************************************************************************
|
|
||||||
|
|
||||||
pub(crate) fn setup(badge: &mut Badge) {
|
|
||||||
let color = badge.props().extra_or(EXTRA_TEXT_BG, ThemeColor::Secondary);
|
|
||||||
badge.alter_prop(PropsOp::replace_classes(
|
|
||||||
"badge",
|
|
||||||
util::join!("badge ", class::TextColor::Bg(color).to_class()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
@ -10,12 +10,10 @@ const EXTRA_WIDTH: &str = "bootsier.container.width";
|
||||||
|
|
||||||
/// Extensión de Bootsier para [`Container`].
|
/// Extensión de Bootsier para [`Container`].
|
||||||
///
|
///
|
||||||
/// Permite establecer el comportamiento del ancho del contenedor usando el método
|
/// Permite definir el comportamiento del ancho del contenedor usando el método
|
||||||
/// [`with_width()`](Self::with_width).
|
/// [`with_width()`](Self::with_width). También acepta clases predefinidas para:
|
||||||
///
|
///
|
||||||
/// También habilita al componente para aceptar clases predefinidas para:
|
/// - Modificar el color de fondo ([`Background`](crate::theme::class::Background)).
|
||||||
///
|
|
||||||
/// - Modificar el color de fondo ([`Bg`](crate::theme::class::Bg)).
|
|
||||||
/// - Definir la apariencia del texto ([`Text`](crate::theme::class::Text)).
|
/// - Definir la apariencia del texto ([`Text`](crate::theme::class::Text)).
|
||||||
/// - Establecer bordes ([`Border`](crate::theme::class::Border)).
|
/// - Establecer bordes ([`Border`](crate::theme::class::Border)).
|
||||||
/// - Redondear las esquinas ([`Rounded`](crate::theme::class::Rounded)).
|
/// - Redondear las esquinas ([`Rounded`](crate::theme::class::Rounded)).
|
||||||
|
|
@ -26,11 +24,11 @@ const EXTRA_WIDTH: &str = "bootsier.container.width";
|
||||||
///
|
///
|
||||||
/// let main = bs::Container::main()
|
/// let main = bs::Container::main()
|
||||||
/// .with_id("main-page")
|
/// .with_id("main-page")
|
||||||
/// .with_width(bs::container::Width::From(BreakPoint::LG))
|
/// .with_width(bs::container::Width::From(token::BreakPoint::LG))
|
||||||
/// .with_prop(PropsOp::add_classes(class::Bg::with(ThemeColor::Light)))
|
/// .with_prop(PropsOp::add_classes(class::Background::with(token::Color::Light)))
|
||||||
/// .with_prop(PropsOp::add_classes(class::Text::with(ThemeColor::Dark)))
|
/// .with_prop(PropsOp::add_classes(class::Text::with(token::Color::Dark)))
|
||||||
/// .with_prop(PropsOp::add_classes(class::Border::with(ScaleSize::One)))
|
/// .with_prop(PropsOp::add_classes(class::Border::with(token::ScaleSize::One)))
|
||||||
/// .with_prop(PropsOp::add_classes(class::Rounded::new()));
|
/// .with_prop(PropsOp::add_classes(class::Rounded::with(token::RoundedRadius::Default)));
|
||||||
/// ```
|
/// ```
|
||||||
pub trait ContainerBootsier {
|
pub trait ContainerBootsier {
|
||||||
/// Establece el comportamiento del ancho para el contenedor.
|
/// Establece el comportamiento del ancho para el contenedor.
|
||||||
|
|
@ -38,15 +36,12 @@ pub trait ContainerBootsier {
|
||||||
/// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de
|
/// Determina si el contenedor aplica los anchos máximos predefinidos para cada punto de
|
||||||
/// ruptura, o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo
|
/// ruptura, o si ocupa siempre el 100% del ancho disponible, o lo hace hasta un ancho máximo
|
||||||
/// explícito. Ver [`Width`] para las variantes disponibles.
|
/// explícito. Ver [`Width`] para las variantes disponibles.
|
||||||
#[builder_fn]
|
|
||||||
fn with_width(self, width: Width) -> Self;
|
fn with_width(self, width: Width) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContainerBootsier for Container {
|
impl ContainerBootsier for Container {
|
||||||
#[builder_fn]
|
fn with_width(self, width: Width) -> Self {
|
||||||
fn with_width(mut self, width: Width) -> Self {
|
self.with_prop(PropsOp::set_extra(EXTRA_WIDTH, width))
|
||||||
self.alter_prop(PropsOp::set_extra(EXTRA_WIDTH, width));
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,7 +56,7 @@ pub enum Width {
|
||||||
Default,
|
Default,
|
||||||
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
|
/// Aplica los anchos máximos predefinidos a partir del punto de ruptura indicado. Por debajo de
|
||||||
/// ese punto de ruptura ocupa el 100% del ancho disponible.
|
/// ese punto de ruptura ocupa el 100% del ancho disponible.
|
||||||
From(BreakPoint),
|
From(token::BreakPoint),
|
||||||
/// Ocupa el 100% del ancho disponible siempre.
|
/// Ocupa el 100% del ancho disponible siempre.
|
||||||
Fluid,
|
Fluid,
|
||||||
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
|
/// Ocupa el 100% del ancho disponible hasta un ancho máximo explícito.
|
||||||
|
|
@ -75,10 +70,10 @@ impl Width {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn push_to(self, classes: &mut String) {
|
pub fn push_to(self, classes: &mut String) {
|
||||||
match self {
|
match self {
|
||||||
Self::Default => BreakPoint::None.push_to(classes, Self::CONTAINER, ""),
|
Self::Default => token::BreakPoint::None.push_to(classes, Self::CONTAINER, ""),
|
||||||
Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""),
|
Self::From(bp) => bp.push_to(classes, Self::CONTAINER, ""),
|
||||||
Self::Fluid | Self::FluidMax(_) => {
|
Self::Fluid | Self::FluidMax(_) => {
|
||||||
BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid")
|
token::BreakPoint::None.push_to(classes, Self::CONTAINER, "fluid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,12 @@ pub trait InputBootsier {
|
||||||
/// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene
|
/// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene
|
||||||
/// contenido. Requiere que el campo tenga un atributo `placeholder` definido; si no se
|
/// contenido. Requiere que el campo tenga un atributo `placeholder` definido; si no se
|
||||||
/// especifica, se fuerza `placeholder=""` antes del renderizado.
|
/// especifica, se fuerza `placeholder=""` antes del renderizado.
|
||||||
#[builder_fn]
|
|
||||||
fn with_floating_label(self, floating: bool) -> Self;
|
fn with_floating_label(self, floating: bool) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputBootsier for Field {
|
impl InputBootsier for Field {
|
||||||
#[builder_fn]
|
fn with_floating_label(self, floating: bool) -> Self {
|
||||||
fn with_floating_label(mut self, floating: bool) -> Self {
|
self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating))
|
||||||
self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating));
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,22 +52,14 @@ pub(crate) fn setup(field: &mut Field) {
|
||||||
pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, ComponentError> {
|
pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let container_id = field.id();
|
let container_id = field.id();
|
||||||
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
|
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
|
||||||
|
let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false);
|
||||||
let input_class = if *field.plaintext() {
|
let input_class = if *field.plaintext() {
|
||||||
"form-control-plaintext"
|
"form-control-plaintext"
|
||||||
} else {
|
} else {
|
||||||
"form-control"
|
"form-control"
|
||||||
};
|
};
|
||||||
let strict = field.kind().is_strict();
|
// La etiqueta flotante requiere `placeholder` para animar la etiqueta; si no está definido, se
|
||||||
let masked = *field.kind() == Kind::StrictPassword;
|
// fuerza `placeholder=""`.
|
||||||
let autocomplete = if strict {
|
|
||||||
Some(form::Autocomplete::Off)
|
|
||||||
} else {
|
|
||||||
field.autocomplete().get()
|
|
||||||
};
|
|
||||||
|
|
||||||
// La etiqueta flotante requiere `placeholder` para animar la etiqueta.
|
|
||||||
let floating = field.props().extra_or(EXTRA_FLOATING_LABEL, false);
|
|
||||||
// Si no está definido, se fuerza `placeholder=""`.
|
|
||||||
let placeholder = if floating {
|
let placeholder = if floating {
|
||||||
Some(field.placeholder().lookup(cx).unwrap_or_default())
|
Some(field.placeholder().lookup(cx).unwrap_or_default())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -92,7 +81,6 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, Componen
|
||||||
},
|
},
|
||||||
None => html! {},
|
None => html! {},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
div (field.props()) {
|
div (field.props()) {
|
||||||
@if !floating { (label) }
|
@if !floating { (label) }
|
||||||
|
|
@ -106,13 +94,9 @@ pub(crate) fn render(field: &Field, cx: &mut Context) -> Result<Markup, Componen
|
||||||
maxlength=[field.maxlength().get()]
|
maxlength=[field.maxlength().get()]
|
||||||
placeholder=[placeholder]
|
placeholder=[placeholder]
|
||||||
inputmode=[field.inputmode().get()]
|
inputmode=[field.inputmode().get()]
|
||||||
autocomplete=[autocomplete]
|
autocomplete=[field.autocomplete().get()]
|
||||||
spellcheck=[strict.then_some("false")]
|
|
||||||
autocorrect=[strict.then_some("off")]
|
|
||||||
style=[masked.then_some("-webkit-text-security: disc; text-security: disc;")]
|
|
||||||
autofocus[*field.autofocus()]
|
autofocus[*field.autofocus()]
|
||||||
readonly[*field.readonly() || *field.plaintext() || strict]
|
readonly[*field.readonly() || *field.plaintext()]
|
||||||
onfocus=[strict.then_some("this.removeAttribute('readonly')")]
|
|
||||||
required[*field.required()]
|
required[*field.required()]
|
||||||
disabled[*field.disabled()];
|
disabled[*field.disabled()];
|
||||||
@if floating { (label) }
|
@if floating { (label) }
|
||||||
|
|
|
||||||
|
|
@ -33,15 +33,12 @@ pub trait SelectBootsier {
|
||||||
/// Si se usa la etiqueta flotante, se anulan los valores establecidos con
|
/// Si se usa la etiqueta flotante, se anulan los valores establecidos con
|
||||||
/// [`with_multiple()`](form::select::Field::with_multiple) y
|
/// [`with_multiple()`](form::select::Field::with_multiple) y
|
||||||
/// [`with_rows()`](form::select::Field::with_rows) antes del renderizado.
|
/// [`with_rows()`](form::select::Field::with_rows) antes del renderizado.
|
||||||
#[builder_fn]
|
|
||||||
fn with_floating_label(self, floating: bool) -> Self;
|
fn with_floating_label(self, floating: bool) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SelectBootsier for Field {
|
impl SelectBootsier for Field {
|
||||||
#[builder_fn]
|
fn with_floating_label(self, floating: bool) -> Self {
|
||||||
fn with_floating_label(mut self, floating: bool) -> Self {
|
self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating))
|
||||||
self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating));
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,15 +31,12 @@ pub trait TextareaBootsier {
|
||||||
///
|
///
|
||||||
/// Si se usa la etiqueta flotante, se anula el valor establecido con
|
/// Si se usa la etiqueta flotante, se anula el valor establecido con
|
||||||
/// [`with_rows()`](form::Textarea::with_rows) antes del renderizado.
|
/// [`with_rows()`](form::Textarea::with_rows) antes del renderizado.
|
||||||
#[builder_fn]
|
|
||||||
fn with_floating_label(self, floating: bool) -> Self;
|
fn with_floating_label(self, floating: bool) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextareaBootsier for Textarea {
|
impl TextareaBootsier for Textarea {
|
||||||
#[builder_fn]
|
fn with_floating_label(self, floating: bool) -> Self {
|
||||||
fn with_floating_label(mut self, floating: bool) -> Self {
|
self.with_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating))
|
||||||
self.alter_prop(PropsOp::set_extra(EXTRA_FLOATING_LABEL, floating));
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,48 +104,32 @@ impl Into<CowStr> for RoundedRadius {
|
||||||
/// - Ajustar el radio de las **esquinas concretas** (`top-start`, `top-end`, `bottom-start`,
|
/// - Ajustar el radio de las **esquinas concretas** (`top-start`, `top-end`, `bottom-start`,
|
||||||
/// `bottom-end`, **en este orden**, respetando LTR/RTL).
|
/// `bottom-end`, **en este orden**, respetando LTR/RTL).
|
||||||
///
|
///
|
||||||
/// # Comportamiento aditivo / sustractivo
|
|
||||||
///
|
|
||||||
/// - **Aditivo**: se parte de [`Rounded::default()`] (sin redondeo) y se van añadiendo lados o
|
|
||||||
/// esquinas concretas con el radio deseado.
|
|
||||||
///
|
|
||||||
/// - **Sustractivo**: se parte de [`Rounded::new()`] o [`Rounded::with`] (radio global ya aplicado)
|
|
||||||
/// y se anulan lados o esquinas concretas con [`RoundedRadius::Zero`].
|
|
||||||
///
|
|
||||||
/// # Ejemplos
|
/// # Ejemplos
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use pagetop_bootsier::theme::*;
|
/// use pagetop_bootsier::theme::*;
|
||||||
///
|
///
|
||||||
/// // Radio global por defecto, equivalente a `Rounded::with(RoundedRadius::Default)`:
|
/// // Radio global:
|
||||||
/// let r = class::Rounded::new();
|
/// let r = class::Rounded::with(class::RoundedRadius::Default);
|
||||||
/// assert_eq!(r.to_class(), "rounded");
|
/// assert_eq!(r.to_class(), "rounded");
|
||||||
///
|
///
|
||||||
/// // Radio global explícito:
|
/// // Sin redondeo:
|
||||||
/// let r = class::Rounded::with(class::RoundedRadius::Scale3);
|
/// let r = class::Rounded::new();
|
||||||
/// assert_eq!(r.to_class(), "rounded-3");
|
|
||||||
///
|
|
||||||
/// // Sin redondeo (comportamiento de `Default`):
|
|
||||||
/// let r = class::Rounded::default();
|
|
||||||
/// assert_eq!(r.to_class(), "");
|
/// assert_eq!(r.to_class(), "");
|
||||||
///
|
///
|
||||||
/// // Aditivo (radio en las esquinas de un lado lógico):
|
/// // Radio en las esquinas de un lado lógico:
|
||||||
/// let r = class::Rounded::default().with_end(class::RoundedRadius::Scale2);
|
/// let r = class::Rounded::new().with_end(class::RoundedRadius::Scale2);
|
||||||
/// assert_eq!(r.to_class(), "rounded-end-2");
|
/// assert_eq!(r.to_class(), "rounded-end-2");
|
||||||
///
|
///
|
||||||
/// // Aditivo (radio en una esquina concreta):
|
/// // Radio en una esquina concreta:
|
||||||
/// let r = class::Rounded::default().with_top_start(class::RoundedRadius::Scale3);
|
/// let r = class::Rounded::new().with_top_start(class::RoundedRadius::Scale3);
|
||||||
/// assert_eq!(r.to_class(), "rounded-top-start-3");
|
/// assert_eq!(r.to_class(), "rounded-top-start-3");
|
||||||
///
|
///
|
||||||
/// // Sustractivo (radio global menos la esquina superior-inicial):
|
|
||||||
/// let r = class::Rounded::new().with_top_start(class::RoundedRadius::Zero);
|
|
||||||
/// assert_eq!(r.to_class(), "rounded rounded-top-start-0");
|
|
||||||
///
|
|
||||||
/// // Combinado (ejemplo completo):
|
/// // Combinado (ejemplo completo):
|
||||||
/// let r = class::Rounded::default()
|
/// let r = class::Rounded::new()
|
||||||
/// .with_top(class::RoundedRadius::Default) // Añade redondeo arriba.
|
/// .with_top(class::RoundedRadius::Default) // Añade redondeo arriba.
|
||||||
/// .with_bottom_start(class::RoundedRadius::Scale4) // Añade esquina redondeada concreta.
|
/// .with_bottom_start(class::RoundedRadius::Scale4) // Añade una esquina redondeada concreta.
|
||||||
/// .with_bottom_end(class::RoundedRadius::Circle); // Añade redondeo máximo en otra esquina.
|
/// .with_bottom_end(class::RoundedRadius::Circle); // Añade redondeo extremo en otra esquina.
|
||||||
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
|
/// assert_eq!(r.to_class(), "rounded-top rounded-bottom-start-4 rounded-bottom-end-circle");
|
||||||
/// ```
|
/// ```
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
|
@ -163,13 +147,12 @@ pub struct Rounded {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rounded {
|
impl Rounded {
|
||||||
/// Prepara las esquinas con el **radio de redondeo por defecto** (`rounded`), como
|
/// Prepara las esquinas **sin redondeo global** de partida.
|
||||||
/// [`Self::with`] con [`RoundedRadius::Default`].
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default().with_radius(RoundedRadius::Default)
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea las esquinas con un **radio global** explícito (`radius`).
|
/// Crea las esquinas **con redondeo global** (`radius`).
|
||||||
pub fn with(radius: RoundedRadius) -> Self {
|
pub fn with(radius: RoundedRadius) -> Self {
|
||||||
Self::default().with_radius(radius)
|
Self::default().with_radius(radius)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
|
|
||||||
mod badge;
|
|
||||||
pub use badge::Badge;
|
|
||||||
|
|
||||||
mod block;
|
mod block;
|
||||||
pub use block::Block;
|
pub use block::Block;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
use crate::prelude::*;
|
|
||||||
|
|
||||||
/// Componente para mostrar una **etiqueta corta informativa** (*badge*).
|
|
||||||
///
|
|
||||||
/// # Ejemplo
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// use pagetop::prelude::*;
|
|
||||||
///
|
|
||||||
/// let badge = Badge::new().with_label(L10n::n("Admin"));
|
|
||||||
/// ```
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
|
||||||
pub struct Badge {
|
|
||||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
|
||||||
props: Props,
|
|
||||||
/// Devuelve la etiqueta del badge.
|
|
||||||
label: L10n,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Component for Badge {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.props.get_id()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup(&mut self, _cx: &Context) {
|
|
||||||
self.alter_prop(PropsOp::prepend_classes("badge"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
|
||||||
Ok(html! {
|
|
||||||
span (self.props()) {
|
|
||||||
(self.label().using(cx))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Badge {
|
|
||||||
// **< Badge BUILDER >**************************************************************************
|
|
||||||
|
|
||||||
/// Establece el identificador único del componente; igual a `with_prop(PropsOp::set_id(id))`.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl Into<CowStr>) -> Self {
|
|
||||||
self.props.alter_id(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Modifica identificador, clases CSS o atributos HTML del componente.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_prop(mut self, op: PropsOp) -> Self {
|
|
||||||
self.props.alter_prop(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la etiqueta del badge.
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_label(mut self, label: L10n) -> Self {
|
|
||||||
self.label = label;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,6 +9,10 @@ use crate::prelude::*;
|
||||||
/// Representa cada casilla de un grupo de casillas de verificación, con una etiqueta localizable
|
/// Representa cada casilla de un grupo de casillas de verificación, con una etiqueta localizable
|
||||||
/// visible. Puede marcarse como seleccionada o deshabilitada de forma independiente al resto.
|
/// visible. Puede marcarse como seleccionada o deshabilitada de forma independiente al resto.
|
||||||
///
|
///
|
||||||
|
/// El parámetro `name` de [`form::check::Item::new()`](Item::new) se combina con el `name` del
|
||||||
|
/// grupo para componer el atributo `name` de la casilla. Por ejemplo, si el grupo tiene
|
||||||
|
/// `name=interests` y el ítem se crea con `name=tech`, la casilla tendrá `name=interests_tech`.
|
||||||
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
/// ```rust,no_run
|
/// ```rust,no_run
|
||||||
|
|
@ -18,8 +22,8 @@ use crate::prelude::*;
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
/// Devuelve el valor enviado al servidor cuando la casilla está marcada.
|
/// Devuelve el nombre que se combina con el del grupo para componer el atributo `name`.
|
||||||
value: AttrValue,
|
name: AttrValue,
|
||||||
/// Devuelve la etiqueta de la casilla.
|
/// Devuelve la etiqueta de la casilla.
|
||||||
label: L10n,
|
label: L10n,
|
||||||
/// Devuelve si la casilla debe aparecer marcada por defecto.
|
/// Devuelve si la casilla debe aparecer marcada por defecto.
|
||||||
|
|
@ -29,10 +33,13 @@ pub struct Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
/// Crea una nueva casilla con el valor y la etiqueta indicados.
|
/// Crea una nueva casilla con el nombre y la etiqueta indicados.
|
||||||
pub fn new(value: impl AsRef<str>, label: L10n) -> Self {
|
///
|
||||||
|
/// El parámetro `name` se combina con el del grupo para componer el atributo `name` de la
|
||||||
|
/// casilla.
|
||||||
|
pub fn new(name: impl AsRef<str>, label: L10n) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value: AttrValue::new(value),
|
name: AttrValue::new(name),
|
||||||
label,
|
label,
|
||||||
checked: false,
|
checked: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
|
@ -58,10 +65,15 @@ impl Item {
|
||||||
|
|
||||||
/// Componente para crear un **grupo de casillas de verificación**.
|
/// Componente para crear un **grupo de casillas de verificación**.
|
||||||
///
|
///
|
||||||
/// Renderiza una lista de opciones de la que el usuario puede marcar cero, una o varias. Todas las
|
/// Renderiza un conjunto de casillas de verificación donde cada casilla puede marcarse de forma
|
||||||
/// casillas comparten el mismo `name` (el del grupo); cada una envía como valor el de su
|
/// independiente. Las casillas se añaden con [`with_item()`](Field::with_item) usando instancias
|
||||||
/// [`form::check::Item`] cuando está marcada. Las opciones se añaden con [`with_item()`]. Si se
|
/// de [`form::check::Item`]. Si se activa el modo en línea con
|
||||||
/// activa el modo en línea con [`with_inline()`], las casillas se disponen horizontalmente.
|
/// [`with_inline()`](Field::with_inline), las casillas se disponen horizontalmente.
|
||||||
|
///
|
||||||
|
/// El atributo `name` de cada casilla se construye automáticamente combinando el `name` del grupo
|
||||||
|
/// y el `name` del [`form::check::Item`] con un guion bajo. Por ejemplo, para el grupo con
|
||||||
|
/// `name=interests` y casillas con `name=art` y `name=tech`, se genera `name=interests_art` y
|
||||||
|
/// `name=interests_tech`.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -76,29 +88,26 @@ impl Item {
|
||||||
/// .with_item(form::check::Item::new("science", L10n::n("Science")).with_checked(true));
|
/// .with_item(form::check::Item::new("science", L10n::n("Science")).with_checked(true));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// El navegador envía una entrada por cada casilla marcada, todas bajo la misma clave (por ejemplo,
|
/// Cada `name` debe ser único y válido como identificador de campo. Cuando el usuario marca una
|
||||||
/// si el usuario marca "Technology" y "Science", `interests=tech&interests=science`) y ninguna si
|
/// casilla, el navegador envía algo como `interests_tech=true`; mientras que si no la marca, no
|
||||||
/// no marca ninguna. El servidor no necesita conocer de antemano qué opciones existían, lo que hace
|
/// envía nada. En el servidor cada campo se deserializa como `bool` con `#[serde(default)]`:
|
||||||
/// de `Field` la opción adecuada también para listas de opciones dinámicas (por ejemplo, cargadas
|
|
||||||
/// de una base de datos). `axum::extract::Form` (basado en `serde_urlencoded`) no deserializa
|
|
||||||
/// claves repetidas en un `Vec<T>`; hace falta un extractor que sí lo haga, como [`serde_qs`]:
|
|
||||||
///
|
///
|
||||||
/// ```rust,ignore
|
/// ```rust,ignore
|
||||||
/// #[derive(serde::Deserialize)]
|
/// #[derive(serde::Deserialize)]
|
||||||
/// struct FormData {
|
/// struct FormData {
|
||||||
/// #[serde(default)]
|
/// #[serde(default)]
|
||||||
/// interests: Vec<String>, // ["tech", "science"], o [] si no se marcó ninguna.
|
/// interests_art: bool,
|
||||||
|
/// #[serde(default)]
|
||||||
|
/// interests_tech: bool,
|
||||||
|
/// #[serde(default)]
|
||||||
|
/// interests_science: bool,
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
|
||||||
/// [`with_item()`]: Field::with_item
|
|
||||||
/// [`with_inline()`]: Field::with_inline
|
|
||||||
/// [`serde_qs`]: https://docs.rs/serde_qs
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Field {
|
pub struct Field {
|
||||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||||
props: Props,
|
props: Props,
|
||||||
/// Devuelve el nombre compartido por todas las casillas del grupo.
|
/// Devuelve el nombre base compartido por todas las casillas del grupo.
|
||||||
name: AttrName,
|
name: AttrName,
|
||||||
/// Devuelve la etiqueta del grupo.
|
/// Devuelve la etiqueta del grupo.
|
||||||
label: Attr<L10n>,
|
label: Attr<L10n>,
|
||||||
|
|
@ -155,13 +164,18 @@ impl Component for Field {
|
||||||
@for (item, i) in self.items().iter().zip(1..) {
|
@for (item, i) in self.items().iter().zip(1..) {
|
||||||
@let i = i.to_string();
|
@let i = i.to_string();
|
||||||
@let item_id = util::join!(&container_id, "-check-", &i);
|
@let item_id = util::join!(&container_id, "-check-", &i);
|
||||||
|
@let item_name = if let Some(item_name) = item.name().get() {
|
||||||
|
util::join!(&name, "_", &item_name)
|
||||||
|
} else {
|
||||||
|
util::join!(&name, "_", &i)
|
||||||
|
};
|
||||||
div class=(item_classes) {
|
div class=(item_classes) {
|
||||||
input
|
input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id=(&item_id)
|
id=(&item_id)
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
name=(&name)
|
name=(&item_name)
|
||||||
value=[item.value().get()]
|
value="true"
|
||||||
checked[*item.checked()]
|
checked[*item.checked()]
|
||||||
disabled[*item.disabled() || *self.disabled()];
|
disabled[*item.disabled() || *self.disabled()];
|
||||||
label class="form-check-label" for=(&item_id) {
|
label class="form-check-label" for=(&item_id) {
|
||||||
|
|
@ -194,11 +208,14 @@ impl Field {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el nombre compartido por todas las casillas del grupo.
|
/// Establece el nombre base para el grupo de casillas.
|
||||||
///
|
///
|
||||||
/// Todas las casillas [`form::check::Item`](Item) del grupo llevarán este mismo `name`. Si se
|
/// Se combina con el `name` de cada [`form::check::Item`](Item) para generar el atributo `name`
|
||||||
/// omite, se asigna un nombre generado automáticamente. Para deserializar los campos en el
|
/// de cada casilla de verificación. Por ejemplo, con `name=interests` en el grupo y `name=tech`
|
||||||
/// servidor es recomendable establecer un `name` explícito.
|
/// en el ítem, se genera `name=interests_tech`.
|
||||||
|
///
|
||||||
|
/// Si se omite, se asigna un nombre generado automáticamente. Para deserializar los campos en
|
||||||
|
/// el servidor es recomendable establecer un `name` explícito.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||||
self.name.alter_name(name);
|
self.name.alter_name(name);
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ use crate::base::component::form;
|
||||||
pub struct Form {
|
pub struct Form {
|
||||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||||
props: Props,
|
props: Props,
|
||||||
/// Devuelve la ruta de destino del formulario.
|
/// Devuelve la URL/ruta de destino del formulario.
|
||||||
action: Route,
|
action: AttrValue,
|
||||||
/// Devuelve el método para enviar el formulario.
|
/// Devuelve el método para enviar el formulario.
|
||||||
method: form::Method,
|
method: form::Method,
|
||||||
/// Devuelve el juego de caracteres aceptado por el formulario.
|
/// Devuelve el juego de caracteres aceptado por el formulario.
|
||||||
|
|
@ -79,7 +79,7 @@ impl Component for Form {
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
form
|
form
|
||||||
(self.props())
|
(self.props())
|
||||||
action=[self.action().try_resolve(cx)]
|
action=[self.action().get()]
|
||||||
method=[method]
|
method=[method]
|
||||||
accept-charset=[self.charset().get()]
|
accept-charset=[self.charset().get()]
|
||||||
{
|
{
|
||||||
|
|
@ -106,13 +106,10 @@ impl Form {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece la ruta de destino del formulario.
|
/// Establece la URL/ruta de destino del formulario.
|
||||||
///
|
|
||||||
/// Acepta un literal, un `String`, o una [`Route`] explícita construida con [`Route::with()`]
|
|
||||||
/// para rutas que dependan del contexto de renderizado.
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_action(mut self, action: impl Into<Route>) -> Self {
|
pub fn with_action(mut self, action: impl AsRef<str>) -> Self {
|
||||||
self.action = action.into();
|
self.action.alter_str(action);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,9 @@ use std::fmt;
|
||||||
/// Tipo de campo para un [`form::input::Field`].
|
/// Tipo de campo para un [`form::input::Field`].
|
||||||
///
|
///
|
||||||
/// Determina el tipo de entrada que acepta, así como el comportamiento del navegador al interactuar
|
/// 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 usando [`text()`] o [`password()`],
|
/// con el campo. Implícitamente se aplica al crear el control: [`text()`](Field::text),
|
||||||
/// [`strict_text()`] o [`strict_password()`], [`search()`], [`email()`], [`telephone()`] o
|
/// [`password()`](Field::password), [`search()`](Field::search), [`email()`](Field::email),
|
||||||
/// [`url()`].
|
/// [`telephone()`](Field::telephone) o [`url()`](Field::url).
|
||||||
///
|
|
||||||
/// [`text()`]: Field::text
|
|
||||||
/// [`password()`]: Field::password
|
|
||||||
/// [`strict_text()`]: Field::strict_text
|
|
||||||
/// [`strict_password()`]: Field::strict_password
|
|
||||||
/// [`search()`]: Field::search
|
|
||||||
/// [`email()`]: Field::email
|
|
||||||
/// [`telephone()`]: Field::telephone
|
|
||||||
/// [`url()`]: Field::url
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||||
pub enum Kind {
|
pub enum Kind {
|
||||||
/// Entrada de texto genérico (`type="text"`). Es el tipo por defecto.
|
/// Entrada de texto genérico (`type="text"`). Es el tipo por defecto.
|
||||||
|
|
@ -28,14 +19,6 @@ pub enum Kind {
|
||||||
Text,
|
Text,
|
||||||
/// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado.
|
/// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado.
|
||||||
Password,
|
Password,
|
||||||
/// Texto genérico blindado contra el autorrelleno del navegador (`type="text"`).
|
|
||||||
///
|
|
||||||
/// Ver [`Field::strict_text()`].
|
|
||||||
StrictText,
|
|
||||||
/// Contraseña blindada contra el autorrelleno del navegador (`type="text"`).
|
|
||||||
///
|
|
||||||
/// Ver [`Field::strict_password()`].
|
|
||||||
StrictPassword,
|
|
||||||
/// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda.
|
/// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda.
|
||||||
Search,
|
Search,
|
||||||
/// Entrada de un correo electrónico (`type="email"`). Permite validar el formato del correo.
|
/// Entrada de un correo electrónico (`type="email"`). Permite validar el formato del correo.
|
||||||
|
|
@ -46,20 +29,10 @@ pub enum Kind {
|
||||||
Url,
|
Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Kind {
|
|
||||||
/// Devuelve `true` si el tipo aplica el conjunto de medidas contra el autorrelleno del
|
|
||||||
/// navegador.
|
|
||||||
///
|
|
||||||
/// Ver [`Field::strict_text()`] y [`Field::strict_password()`].
|
|
||||||
pub fn is_strict(self) -> bool {
|
|
||||||
matches!(self, Kind::StrictText | Kind::StrictPassword)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Kind {
|
impl fmt::Display for Kind {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(match self {
|
f.write_str(match self {
|
||||||
Kind::Text | Kind::StrictText | Kind::StrictPassword => "text",
|
Kind::Text => "text",
|
||||||
Kind::Password => "password",
|
Kind::Password => "password",
|
||||||
Kind::Search => "search",
|
Kind::Search => "search",
|
||||||
Kind::Email => "email",
|
Kind::Email => "email",
|
||||||
|
|
@ -121,8 +94,6 @@ impl fmt::Display for Mode {
|
||||||
///
|
///
|
||||||
/// - [`Field::text()`]: campo de texto genérico (`type="text"`, por defecto).
|
/// - [`Field::text()`]: campo de texto genérico (`type="text"`, por defecto).
|
||||||
/// - [`Field::password()`]: contraseña (`type="password"`).
|
/// - [`Field::password()`]: contraseña (`type="password"`).
|
||||||
/// - [`Field::strict_text()`]: texto genérico blindado contra el autorrelleno del navegador.
|
|
||||||
/// - [`Field::strict_password()`]: contraseña blindada contra el autorrelleno del navegador.
|
|
||||||
/// - [`Field::search()`]: búsqueda (`type="search"`).
|
/// - [`Field::search()`]: búsqueda (`type="search"`).
|
||||||
/// - [`Field::email()`]: correo electrónico (`type="email"`).
|
/// - [`Field::email()`]: correo electrónico (`type="email"`).
|
||||||
/// - [`Field::telephone()`]: teléfono (`type="tel"`).
|
/// - [`Field::telephone()`]: teléfono (`type="tel"`).
|
||||||
|
|
@ -205,9 +176,6 @@ impl Component for Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clases CSS del contenedor del campo de texto.
|
// Clases CSS del contenedor del campo de texto.
|
||||||
if self.kind().is_strict() {
|
|
||||||
self.alter_prop(PropsOp::prepend_classes("form-field-strict"));
|
|
||||||
}
|
|
||||||
self.alter_prop(PropsOp::prepend_classes(util::join!(
|
self.alter_prop(PropsOp::prepend_classes(util::join!(
|
||||||
"form-field form-field-",
|
"form-field form-field-",
|
||||||
self.kind().to_string()
|
self.kind().to_string()
|
||||||
|
|
@ -222,13 +190,6 @@ impl Component for Field {
|
||||||
} else {
|
} else {
|
||||||
"form-control"
|
"form-control"
|
||||||
};
|
};
|
||||||
let strict = self.kind().is_strict();
|
|
||||||
let masked = *self.kind() == Kind::StrictPassword;
|
|
||||||
let autocomplete = if strict {
|
|
||||||
Some(form::Autocomplete::Off)
|
|
||||||
} else {
|
|
||||||
self.autocomplete().get()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
div (self.props()) {
|
div (self.props()) {
|
||||||
|
|
@ -255,13 +216,9 @@ impl Component for Field {
|
||||||
maxlength=[self.maxlength().get()]
|
maxlength=[self.maxlength().get()]
|
||||||
placeholder=[self.placeholder().lookup(cx)]
|
placeholder=[self.placeholder().lookup(cx)]
|
||||||
inputmode=[self.inputmode().get()]
|
inputmode=[self.inputmode().get()]
|
||||||
autocomplete=[autocomplete]
|
autocomplete=[self.autocomplete().get()]
|
||||||
spellcheck=[strict.then_some("false")]
|
|
||||||
autocorrect=[strict.then_some("off")]
|
|
||||||
style=[masked.then_some("-webkit-text-security: disc; text-security: disc;")]
|
|
||||||
autofocus[*self.autofocus()]
|
autofocus[*self.autofocus()]
|
||||||
readonly[*self.readonly() || *self.plaintext() || strict]
|
readonly[*self.readonly() || *self.plaintext()]
|
||||||
onfocus=[strict.then_some("this.removeAttribute('readonly')")]
|
|
||||||
required[*self.required()]
|
required[*self.required()]
|
||||||
disabled[*self.disabled()];
|
disabled[*self.disabled()];
|
||||||
@if let Some(description) = self.help_text().lookup(cx) {
|
@if let Some(description) = self.help_text().lookup(cx) {
|
||||||
|
|
@ -293,55 +250,6 @@ impl Field {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un campo de **texto genérico blindado** contra el autorrelleno del navegador.
|
|
||||||
///
|
|
||||||
/// Algunos navegadores rellenan los campos de texto con datos ya guardados (como usuario,
|
|
||||||
/// email, etc.) aplicando heurísticas por nombre o posición, incluso con `autocomplete="off"`.
|
|
||||||
/// Este método fuerza `autocomplete="off"`, desactiva corrección ortográfica y autocorrección,
|
|
||||||
/// y permanece de sólo lectura hasta que el usuario hace foco (ya que normalmente el navegador
|
|
||||||
/// respeta el `readonly` en la carga aunque ignore `autocomplete="off"`).
|
|
||||||
///
|
|
||||||
/// Soporte por motor de navegador (Blink: Chrome, Edge, Opera; Gecko: Firefox; WebKit: Safari):
|
|
||||||
///
|
|
||||||
/// | Medida | Blink | Gecko | WebKit |
|
|
||||||
/// |-------------------------------|------------|------------|--------------------------|
|
|
||||||
/// | `autocomplete="off"` ignorado | Sí | Sí | Lo respeta mejor |
|
|
||||||
/// | `spellcheck="false"` | Sí | Sí | Sí |
|
|
||||||
/// | `autocorrect="off"` | Sin efecto | Sin efecto | Sí (origen del atributo) |
|
|
||||||
///
|
|
||||||
/// El truco `readonly` + `onfocus` compensa específicamente la agresividad de Chrome/Blink al
|
|
||||||
/// autorrellenar campos nada más cargar la página; su fiabilidad varía entre versiones y no
|
|
||||||
/// está documentada de forma oficial. Ninguna de estas medidas es infalible frente a gestores
|
|
||||||
/// de contraseñas de terceros, que usan heurísticas propias.
|
|
||||||
///
|
|
||||||
/// Ver también [`Field::strict_password()`].
|
|
||||||
pub fn strict_text() -> Self {
|
|
||||||
Self {
|
|
||||||
kind: Kind::StrictText,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un campo de **contraseña blindada** contra el autorrelleno del navegador.
|
|
||||||
///
|
|
||||||
/// Aplica las mismas medidas que [`Field::strict_text()`] y, además, enmascara el contenido
|
|
||||||
/// visualmente vía CSS (usando `-webkit-text-security`) en lugar de usar `type="password"`. Al
|
|
||||||
/// no ser un campo de contraseña para el navegador, evita el gestor nativo de contraseñas, el
|
|
||||||
/// icono de mostrar/ocultar y el aviso de guardar la contraseña.
|
|
||||||
///
|
|
||||||
/// Útil en formularios de acceso donde ese comportamiento no es deseable (como puestos
|
|
||||||
/// compartidos o paneles administrativos sensibles).
|
|
||||||
///
|
|
||||||
/// La propiedad `-webkit-text-security` está soportada de forma nativa en Blink (Chrome, Edge,
|
|
||||||
/// Opera) y en WebKit (Safari, su origen); Firefox lo soporta desde la versión 114 (2023) con
|
|
||||||
/// el mismo nombre.
|
|
||||||
pub fn strict_password() -> Self {
|
|
||||||
Self {
|
|
||||||
kind: Kind::StrictPassword,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Crea un campo de **búsqueda** (`type="search"`).
|
/// Crea un campo de **búsqueda** (`type="search"`).
|
||||||
///
|
///
|
||||||
/// Semánticamente equivalente a `text` pero optimizado para búsquedas: algunos navegadores
|
/// Semánticamente equivalente a `text` pero optimizado para búsquedas: algunos navegadores
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ impl fmt::Debug for Region {
|
||||||
impl Default for Region {
|
impl Default for Region {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Region {
|
Region {
|
||||||
region: &CoreRegions::Content,
|
region: &CoreRegion::Content,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,33 +90,17 @@ impl Component for Region {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Region {
|
impl Region {
|
||||||
/// Define el componente que renderizará [`CoreRegions::Header`].
|
/// Define el componente que renderizará [`CoreRegion::Header`].
|
||||||
pub fn header() -> Self {
|
pub fn header() -> Self {
|
||||||
Region {
|
Region {
|
||||||
region: &CoreRegions::Header,
|
region: &CoreRegion::Header,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Define el componente que renderizará [`CoreRegions::Aside`].
|
/// Define el componente que renderizará [`CoreRegion::Footer`].
|
||||||
pub fn aside() -> Self {
|
|
||||||
Region {
|
|
||||||
region: &CoreRegions::Aside,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el componente que renderizará [`CoreRegions::Content`].
|
|
||||||
///
|
|
||||||
/// Equivale a [`Self::new()`] o [`Self::default()`], que ya usan esta región por defecto.
|
|
||||||
pub fn content() -> Self {
|
|
||||||
Region {
|
|
||||||
region: &CoreRegions::Content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el componente que renderizará [`CoreRegions::Footer`].
|
|
||||||
pub fn footer() -> Self {
|
pub fn footer() -> Self {
|
||||||
Region {
|
Region {
|
||||||
region: &CoreRegions::Footer,
|
region: &CoreRegion::Footer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,32 +4,25 @@ use std::fmt;
|
||||||
|
|
||||||
/// Componente que renderiza el cuerpo de una plantilla de regiones.
|
/// Componente que renderiza el cuerpo de una plantilla de regiones.
|
||||||
///
|
///
|
||||||
/// La composición por defecto usa el componente [`Region`] para mostrar, en este orden, las
|
/// La composición por defecto usa el componente [`Region`](crate::base::component::layout::Region)
|
||||||
/// regiones [`CoreRegions::Header`], [`CoreRegions::Aside`], [`CoreRegions::Content`] y
|
/// para mostrar, en este orden, las regiones [`CoreRegion::Header`], [`CoreRegion::Content`] y
|
||||||
/// [`CoreRegions::Footer`], envueltas en un contenedor `div.wrapper` que un tema puede maquetar a
|
/// [`CoreRegion::Footer`].
|
||||||
/// su gusto (por ejemplo, usando CSS Grid, para que `Aside` se muestre como columna lateral junto a
|
|
||||||
/// `Content`). Si `Aside`, o cualquier otra región, no tiene contenido, no se renderiza.
|
|
||||||
///
|
///
|
||||||
/// No incluye las regiones reservadas ([`ReservedRegions::PageTop`] y
|
/// No incluye las regiones reservadas
|
||||||
/// [`ReservedRegions::PageBottom`]) porque el propio [`Page::render()`] las añade antes y después
|
/// [`ReservedRegion::PageTop`](crate::response::ReservedRegion::PageTop) y
|
||||||
/// del resultado de [`Theme::render_page_body()`] para que se rendericen siempre,
|
/// [`ReservedRegion::PageBottom`](crate::response::ReservedRegion::PageBottom) porque el propio
|
||||||
/// independientemente de la plantilla que se use.
|
/// [`Page::render()`](crate::response::Page::render) las añade antes y después del resultado de
|
||||||
|
/// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para que se
|
||||||
|
/// rendericen siempre, independientemente de la plantilla que se use.
|
||||||
///
|
///
|
||||||
/// Si un tema necesita maquetar una plantilla determinada de forma distinta, puede capturar este
|
/// Si un tema necesita maquetar una plantilla determinada de forma distinta, puede capturar este
|
||||||
/// componente en [`Theme::handle_component()`] y hacer [`downcast_ref()`] sobre el [`TemplateRef`]
|
/// componente en [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) y hacer
|
||||||
/// que devuelve [`Self::template()`], para compararlo con la variante deseada.
|
/// [`downcast_ref()`](crate::core::AnyCast::downcast_ref) sobre el [`TemplateRef`] que devuelve
|
||||||
|
/// [`Self::template()`], para compararlo con la variante deseada.
|
||||||
///
|
///
|
||||||
/// Como cualquier otro componente, participa también en el despacho de las
|
/// Como cualquier otro componente, participa también en el despacho de las
|
||||||
/// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan
|
/// [acciones de componentes](crate::base::action::component) para que otras extensiones puedan
|
||||||
/// intervenir en su renderizado.
|
/// intervenir en su renderizado.
|
||||||
///
|
|
||||||
/// [`Region`]: crate::base::component::layout::Region
|
|
||||||
/// [`ReservedRegions::PageTop`]: crate::response::ReservedRegions::PageTop
|
|
||||||
/// [`ReservedRegions::PageBottom`]: crate::response::ReservedRegions::PageBottom
|
|
||||||
/// [`Page::render()`]: crate::response::Page::render
|
|
||||||
/// [`Theme::render_page_body()`]: crate::core::theme::Theme::render_page_body
|
|
||||||
/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component
|
|
||||||
/// [`downcast_ref()`]: crate::core::AnyCast::downcast_ref
|
|
||||||
#[derive(Clone, Getters)]
|
#[derive(Clone, Getters)]
|
||||||
pub struct Template {
|
pub struct Template {
|
||||||
/// Devuelve la plantilla subyacente.
|
/// Devuelve la plantilla subyacente.
|
||||||
|
|
@ -48,7 +41,7 @@ impl fmt::Debug for Template {
|
||||||
impl Default for Template {
|
impl Default for Template {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Template {
|
Template {
|
||||||
template: &CoreTemplates::Standard,
|
template: &CoreTemplate::Standard,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,39 +58,19 @@ impl Component for Template {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let body = html! {
|
|
||||||
(layout::Region::header().render(cx).await)
|
|
||||||
(layout::Region::aside().render(cx).await)
|
|
||||||
(layout::Region::content().render(cx).await)
|
|
||||||
(layout::Region::footer().render(cx).await)
|
|
||||||
};
|
|
||||||
|
|
||||||
if body.is_empty() {
|
|
||||||
return Ok(html! {});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
div.wrapper {
|
(layout::Region::header().render(cx).await)
|
||||||
(body)
|
(layout::Region::default().render(cx).await)
|
||||||
}
|
(layout::Region::footer().render(cx).await)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Template {
|
impl Template {
|
||||||
/// Define el componente que renderizará [`CoreTemplates::Standard`].
|
/// Define el componente que renderizará [`CoreTemplate::Admin`].
|
||||||
///
|
|
||||||
/// Equivale a [`Self::new()`] o [`Self::default()`], que ya usan esta plantilla por defecto.
|
|
||||||
pub fn standard() -> Self {
|
|
||||||
Template {
|
|
||||||
template: &CoreTemplates::Standard,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Define el componente que renderizará [`CoreTemplates::Admin`].
|
|
||||||
pub fn admin() -> Self {
|
pub fn admin() -> Self {
|
||||||
Template {
|
Template {
|
||||||
template: &CoreTemplates::Admin,
|
template: &CoreTemplate::Admin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ impl Theme for Basic {
|
||||||
.with_weight(-99),
|
.with_weight(-99),
|
||||||
))
|
))
|
||||||
.alter_child_in(
|
.alter_child_in(
|
||||||
&CoreRegions::Footer,
|
&CoreRegion::Footer,
|
||||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::auth::CurrentUser;
|
||||||
use crate::core::TypeInfo;
|
use crate::core::TypeInfo;
|
||||||
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
|
use crate::core::component::{ChildOp, Component, MessageLevel, StatusMessage};
|
||||||
use crate::core::theme::all::DEFAULT_THEME;
|
use crate::core::theme::all::DEFAULT_THEME;
|
||||||
use crate::core::theme::{ChildrenInRegions, CoreRegions, CoreTemplates};
|
use crate::core::theme::{ChildrenInRegions, CoreRegion, CoreTemplate};
|
||||||
use crate::core::theme::{RegionRef, TemplateRef, ThemeRef};
|
use crate::core::theme::{RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, Preload, StyleSheet};
|
||||||
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
use crate::html::{Markup, Props, PropsOp, RoutePath, html};
|
||||||
|
|
@ -77,7 +77,7 @@ pub enum ContextError {
|
||||||
/// # use pagetop_aliner::Aliner;
|
/// # use pagetop_aliner::Aliner;
|
||||||
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
/// fn prepare_context<C: Contextual>(cx: C) -> C {
|
||||||
/// cx.with_langid(&Locale::resolve("es-ES"))
|
/// cx.with_langid(&Locale::resolve("es-ES"))
|
||||||
/// .with_template(&CoreTemplates::Standard)
|
/// .with_template(&CoreTemplate::Standard)
|
||||||
/// .with_theme(&Aliner)
|
/// .with_theme(&Aliner)
|
||||||
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
/// .with_assets(AssetsOp::SetFavicon(Some(Favicon::new().with_icon("/favicon.ico"))))
|
||||||
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
/// .with_assets(AssetsOp::AddStyleSheet(StyleSheet::from("/css/app.css")))
|
||||||
|
|
@ -330,7 +330,7 @@ pub struct Context {
|
||||||
|
|
||||||
impl Default for Context {
|
impl Default for Context {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::base(None, &CoreTemplates::Standard)
|
Self::base(None, &CoreTemplate::Standard)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -369,15 +369,15 @@ impl Context {
|
||||||
/// Para un contexto sin petición (renderizar un componente de forma aislada, en tests o fuera
|
/// Para un contexto sin petición (renderizar un componente de forma aislada, en tests o fuera
|
||||||
/// del ciclo de una petición web), usa [`Context::default()`].
|
/// del ciclo de una petición web), usa [`Context::default()`].
|
||||||
pub fn new(request: HttpRequest) -> Self {
|
pub fn new(request: HttpRequest) -> Self {
|
||||||
Self::base(Some(request), &CoreTemplates::Standard)
|
Self::base(Some(request), &CoreTemplate::Standard)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un nuevo contexto asociado a una petición HTTP, con la plantilla de administración.
|
/// Crea un nuevo contexto asociado a una petición HTTP, con la plantilla de administración.
|
||||||
///
|
///
|
||||||
/// El contexto inicializa el idioma, el tema y la plantilla [`CoreTemplates::Admin`], sin
|
/// El contexto inicializa el idioma, el tema y la plantilla [`CoreTemplate::Admin`], sin
|
||||||
/// favicon ni otros recursos cargados.
|
/// favicon ni otros recursos cargados.
|
||||||
pub fn admin(request: HttpRequest) -> Self {
|
pub fn admin(request: HttpRequest) -> Self {
|
||||||
Self::base(Some(request), &CoreTemplates::Admin)
|
Self::base(Some(request), &CoreTemplate::Admin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extrae el `CurrentUser` inyectado por middleware en las extensiones de la petición, o
|
// Extrae el `CurrentUser` inyectado por middleware en las extensiones de la petición, o
|
||||||
|
|
@ -629,8 +629,7 @@ impl Contextual for Context {
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.regions
|
self.regions.alter_child_in(&CoreRegion::Content, op.into());
|
||||||
.alter_child_in(&CoreRegions::Content, op.into());
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,14 +44,15 @@
|
||||||
//! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por
|
//! Un tema puede personalizarse en tres pasos, cada uno necesario sólo si lo que ofrece PageTop por
|
||||||
//! defecto no basta:
|
//! defecto no basta:
|
||||||
//!
|
//!
|
||||||
//! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegions`] (`Header`, `Aside`,
|
//! 1. **Definir regiones nuevas**. Por defecto, PageTop define [`CoreRegion`] (`Header`, `Content`,
|
||||||
//! `Content`, `Footer`) como regiones de plantilla siempre disponibles, y [`ReservedRegions`]
|
//! `Footer`) como las regiones de plantilla que se asumen siempre disponibles, y
|
||||||
//! (`PageTop`, `PageBottom`) como regiones reservadas que se renderizan al margen de cualquier
|
//! [`ReservedRegion`](crate::response::ReservedRegion) (`PageTop`, `PageBottom`) como las
|
||||||
//! plantilla. Un tema puede definir su propio *enum* que implemente [`RegionName`] para
|
//! regiones reservadas que se renderizan al margen de cualquier plantilla. Un tema puede definir
|
||||||
//! **añadir** nuevas regiones que PageTop no ofrece (como barras laterales, regiones específicas
|
//! su propio *enum* que implemente [`RegionName`] para **añadir** nuevas regiones que PageTop no
|
||||||
//! para menús, sliders, cabeceras hero, etc.). No es necesario redefinir las de [`CoreRegions`]
|
//! ofrece (por ejemplo, una barra lateral). No es necesario redefinir las de [`CoreRegion`] ni
|
||||||
//! ni las de [`ReservedRegions`], que ya existen y se asume que cualquier tema respeta.
|
//! las de [`ReservedRegion`](crate::response::ReservedRegion), que ya existen y se asume que
|
||||||
//! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplates`], con las plantillas
|
//! cualquier tema respeta.
|
||||||
|
//! 2. **Definir plantillas nuevas**. Por defecto existe [`CoreTemplate`], con las plantillas
|
||||||
//! `Standard` y `Admin` que usan `Page::new()` y `Page::admin()` respectivamente, y que son
|
//! `Standard` y `Admin` que usan `Page::new()` y `Page::admin()` respectivamente, y que son
|
||||||
//! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada
|
//! siempre las mismas: no hay un método de `Theme` para elegir una plantilla predeterminada
|
||||||
//! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para
|
//! distinta. Un tema puede definir su propio *enum* que implemente [`TemplateName`] para
|
||||||
|
|
@ -65,7 +66,7 @@
|
||||||
//! ([`Region::region()`](crate::base::component::layout::Region::region) o
|
//! ([`Region::region()`](crate::base::component::layout::Region::region) o
|
||||||
//! [`Template::template()`](crate::base::component::layout::Template::template)) con
|
//! [`Template::template()`](crate::base::component::layout::Template::template)) con
|
||||||
//! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por
|
//! [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia el tipo concreto (por
|
||||||
//! ejemplo, [`CoreTemplates`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente
|
//! ejemplo, [`CoreTemplate`] o el propio *enum* del tema). `pagetop-bootsier` hace exactamente
|
||||||
//! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias
|
//! esto para maquetar `Standard` y `Admin` de forma distinta, sin necesitar sus propias
|
||||||
//! variantes de plantilla.
|
//! variantes de plantilla.
|
||||||
//!
|
//!
|
||||||
|
|
@ -95,7 +96,7 @@
|
||||||
//!
|
//!
|
||||||
//! ```rust,no_run
|
//! ```rust,no_run
|
||||||
//! # use pagetop::prelude::*;
|
//! # use pagetop::prelude::*;
|
||||||
//! InRegion::Global(&CoreRegions::Footer).add(PoweredBy::new());
|
//! InRegion::Global(&CoreRegion::Footer).add(PoweredBy::new());
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del
|
//! El componente se guarda como **prototipo**: cada página recibe un clon fresco en el momento del
|
||||||
|
|
@ -107,8 +108,6 @@
|
||||||
//! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una
|
//! [ciclo de renderizado](crate::core::component::ComponentRender). Esto permite registrarlo una
|
||||||
//! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o
|
//! sola vez y que decida por sí mismo cuándo mostrarse, por ejemplo según la ruta de la petición o
|
||||||
//! si el usuario actual está autenticado.
|
//! si el usuario actual está autenticado.
|
||||||
//!
|
|
||||||
//! [`ReservedRegions`]: crate::response::ReservedRegions
|
|
||||||
|
|
||||||
use crate::AutoDefault;
|
use crate::AutoDefault;
|
||||||
use crate::core::AnyInfo;
|
use crate::core::AnyInfo;
|
||||||
|
|
@ -118,30 +117,26 @@ use crate::locale::L10n;
|
||||||
|
|
||||||
/// Interfaz común para las regiones lógicas del `<body>`.
|
/// Interfaz común para las regiones lógicas del `<body>`.
|
||||||
///
|
///
|
||||||
/// Una [`RegionName`] representa un contenedor lógico identificado por un nombre de región. Su
|
/// Una `RegionName` representa un contenedor lógico identificado por un nombre de región. Su
|
||||||
/// contenido se obtiene del [`Context`], donde los componentes suelen registrarse usando
|
/// contenido se obtiene del [`Context`](crate::core::component::Context), donde los componentes
|
||||||
/// implementaciones de métodos como [`Contextual::with_child_in()`].
|
/// suelen registrarse usando implementaciones de métodos como
|
||||||
|
/// [`Contextual::with_child_in()`](crate::core::component::Contextual::with_child_in).
|
||||||
///
|
///
|
||||||
/// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas
|
/// El contenido de una región viene determinado únicamente por su nombre, no por su tipo. Distintas
|
||||||
/// implementaciones de [`RegionName`] que devuelvan el mismo nombre comparten el mismo conjunto de
|
/// implementaciones de [`RegionName`] que devuelvan el mismo nombre comparten el mismo conjunto de
|
||||||
/// componentes registrados en el [`Context`]. Un *enum* propio que implemente [`RegionName`] está
|
/// componentes registrados en el [`Context`](crate::core::component::Context). Un *enum* propio que
|
||||||
/// pensado para **añadir** regiones que PageTop no ofrece (con un nombre propio que no colisione
|
/// implemente [`RegionName`] está pensado para **añadir** regiones que PageTop no ofrece (con un
|
||||||
/// con los de [`CoreRegions`] o [`ReservedRegions`]).
|
/// nombre propio que no colisione con los de [`CoreRegion`] o
|
||||||
|
/// [`ReservedRegion`](crate::response::ReservedRegion)).
|
||||||
///
|
///
|
||||||
/// El tema decide qué regiones mostrar en el `<body>`, normalmente usando una plantilla
|
/// El tema decide qué regiones mostrar en el `<body>`, normalmente usando una plantilla
|
||||||
/// ([`TemplateName`]) al renderizar la página ([`Page`]).
|
/// ([`TemplateName`]) al renderizar la página ([`Page`](crate::response::Page)).
|
||||||
///
|
///
|
||||||
/// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante
|
/// Requiere [`AnyInfo`] para que un [`RegionRef`] pueda recuperarse mediante
|
||||||
/// [`AnyCast::downcast_ref()`] hacia su tipo concreto (por ejemplo, para que un tema distinga en
|
/// [`AnyCast::downcast_ref()`](crate::core::AnyCast::downcast_ref) hacia su tipo concreto (por
|
||||||
/// [`Theme::handle_component()`] qué variante concreta está renderizando el componente [`Region`]).
|
/// ejemplo, para que un tema distinga en
|
||||||
///
|
/// [`Theme::handle_component()`](crate::core::theme::Theme::handle_component) qué variante
|
||||||
/// [`Context`]: crate::core::component::Context
|
/// concreta está renderizando el componente [`Region`](crate::base::component::layout::Region)).
|
||||||
/// [`Contextual::with_child_in()`]: crate::core::component::Contextual::with_child_in
|
|
||||||
/// [`ReservedRegions`]: crate::response::ReservedRegions
|
|
||||||
/// [`Page`]: crate::response::Page
|
|
||||||
/// [`AnyCast::downcast_ref()`]: crate::core::AnyCast::downcast_ref
|
|
||||||
/// [`Theme::handle_component()`]: crate::core::theme::Theme::handle_component
|
|
||||||
/// [`Region`]: crate::base::component::layout::Region
|
|
||||||
pub trait RegionName: Send + Sync + AnyInfo {
|
pub trait RegionName: Send + Sync + AnyInfo {
|
||||||
/// Devuelve el nombre de la región.
|
/// Devuelve el nombre de la región.
|
||||||
///
|
///
|
||||||
|
|
@ -161,34 +156,24 @@ pub trait RegionName: Send + Sync + AnyInfo {
|
||||||
/// Referencia estática a una región.
|
/// Referencia estática a una región.
|
||||||
pub type RegionRef = &'static dyn RegionName;
|
pub type RegionRef = &'static dyn RegionName;
|
||||||
|
|
||||||
// **< CoreRegions >********************************************************************************
|
// **< CoreRegion >*********************************************************************************
|
||||||
|
|
||||||
/// Regiones básicas que PageTop proporciona por defecto.
|
/// Regiones básicas que PageTop proporciona por defecto.
|
||||||
///
|
///
|
||||||
/// Comparten sus nombres (`"header"`, `"aside"`, `"content"`, `"footer"`) con otras regiones que
|
/// Comparten sus nombres (`"header"`, `"content"`, `"footer"`) con otras regiones que implementen
|
||||||
/// implementen [`RegionName`], por lo que comparten también el contenido registrado bajo esos
|
/// [`RegionName`], por lo que comparten también el contenido registrado bajo esos nombres. Por
|
||||||
/// nombres. Por defecto, son las regiones usadas por [`Template`].
|
/// defecto, son las regiones usadas por [`Template`](crate::base::component::layout::Template).
|
||||||
///
|
///
|
||||||
/// A estas regiones hay que sumar también las regiones internas reservadas por [`ReservedRegions`]
|
/// A estas regiones hay que sumar también las regiones internas reservadas por
|
||||||
/// (`"page-top"` y `"page-bottom"`), que [`Page::render()`] renderiza en cualquier caso.
|
/// [`ReservedRegion`](crate::response::ReservedRegion) (`"page-top"` y `"page-bottom"`), que
|
||||||
///
|
/// [`Page::render()`](crate::response::Page::render) renderiza en cualquier caso.
|
||||||
/// [`Template`]: crate::base::component::layout::Template
|
|
||||||
/// [`ReservedRegions`]: crate::response::ReservedRegions
|
|
||||||
/// [`Page::render()`]: crate::response::Page::render
|
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub enum CoreRegions {
|
pub enum CoreRegion {
|
||||||
/// Región estándar para la **cabecera** del documento, de nombre `"header"`.
|
/// Región estándar para la **cabecera** del documento, de nombre `"header"`.
|
||||||
///
|
///
|
||||||
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
|
/// Suele emplearse para mostrar un logotipo, navegación principal, barras superiores, etc.
|
||||||
Header,
|
Header,
|
||||||
|
|
||||||
/// Región de **contenido secundario**, de nombre `"aside"`.
|
|
||||||
///
|
|
||||||
/// Se renderiza por defecto entre `Header` y `Content`. Un tema podría maquetarla, por ejemplo,
|
|
||||||
/// como columna lateral junto a `Content` y emplearla para menús secundarios o cualquier otro
|
|
||||||
/// contenido complementario al principal.
|
|
||||||
Aside,
|
|
||||||
|
|
||||||
/// Región principal de **contenido**, de nombre `"content"`.
|
/// Región principal de **contenido**, de nombre `"content"`.
|
||||||
///
|
///
|
||||||
/// Es la región donde se renderiza el contenido principal del documento. En general será la
|
/// Es la región donde se renderiza el contenido principal del documento. En general será la
|
||||||
|
|
@ -202,12 +187,11 @@ pub enum CoreRegions {
|
||||||
Footer,
|
Footer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegionName for CoreRegions {
|
impl RegionName for CoreRegion {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Header => "header",
|
Self::Header => "header",
|
||||||
Self::Aside => "aside",
|
|
||||||
Self::Content => "content",
|
Self::Content => "content",
|
||||||
Self::Footer => "footer",
|
Self::Footer => "footer",
|
||||||
}
|
}
|
||||||
|
|
@ -216,10 +200,9 @@ impl RegionName for CoreRegions {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn label(&self) -> L10n {
|
fn label(&self) -> L10n {
|
||||||
match self {
|
match self {
|
||||||
Self::Header => L10n::l("region_header"),
|
Self::Header => L10n::l("region-header"),
|
||||||
Self::Aside => L10n::l("region_aside"),
|
Self::Content => L10n::l("region-content"),
|
||||||
Self::Content => L10n::l("region_content"),
|
Self::Footer => L10n::l("region-footer"),
|
||||||
Self::Footer => L10n::l("region_footer"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -248,11 +231,11 @@ pub trait TemplateName: Send + Sync + AnyInfo {
|
||||||
/// Referencia estática a una plantilla.
|
/// Referencia estática a una plantilla.
|
||||||
pub type TemplateRef = &'static dyn TemplateName;
|
pub type TemplateRef = &'static dyn TemplateName;
|
||||||
|
|
||||||
// **< CoreTemplates >******************************************************************************
|
// **< CoreTemplate >*******************************************************************************
|
||||||
|
|
||||||
/// Plantillas que PageTop proporciona por defecto.
|
/// Plantillas que PageTop proporciona por defecto.
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub enum CoreTemplates {
|
pub enum CoreTemplate {
|
||||||
/// Plantilla predeterminada, de nombre `"standard"`.
|
/// Plantilla predeterminada, de nombre `"standard"`.
|
||||||
///
|
///
|
||||||
/// Se emplea cuando no se selecciona ninguna otra plantilla explícitamente.
|
/// Se emplea cuando no se selecciona ninguna otra plantilla explícitamente.
|
||||||
|
|
@ -265,7 +248,7 @@ pub enum CoreTemplates {
|
||||||
Admin,
|
Admin,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateName for CoreTemplates {
|
impl TemplateName for CoreTemplate {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::base::component::{Html, Intro, IntroOpening, layout};
|
||||||
use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender};
|
use crate::core::component::{ChildOp, Component, ComponentError, ComponentRender};
|
||||||
use crate::core::component::{Context, Contextual};
|
use crate::core::component::{Context, Contextual};
|
||||||
use crate::core::extension::Extension;
|
use crate::core::extension::Extension;
|
||||||
use crate::core::theme::CoreRegions;
|
use crate::core::theme::CoreRegion;
|
||||||
use crate::global;
|
use crate::global;
|
||||||
use crate::html::{Markup, html};
|
use crate::html::{Markup, html};
|
||||||
use crate::locale::L10n;
|
use crate::locale::L10n;
|
||||||
|
|
@ -84,10 +84,9 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// regiones.
|
/// regiones.
|
||||||
///
|
///
|
||||||
/// Con la configuración por defecto, la plantilla estándar utiliza las regiones
|
/// Con la configuración por defecto, la plantilla estándar utiliza las regiones
|
||||||
/// [`CoreRegions::Header`](crate::core::theme::CoreRegions::Header),
|
/// [`CoreRegion::Header`](crate::core::theme::CoreRegion::Header),
|
||||||
/// [`CoreRegions::Aside`](crate::core::theme::CoreRegions::Aside),
|
/// [`CoreRegion::Content`](crate::core::theme::CoreRegion::Content) y
|
||||||
/// [`CoreRegions::Content`](crate::core::theme::CoreRegions::Content) y
|
/// [`CoreRegion::Footer`](crate::core::theme::CoreRegion::Footer) en ese orden.
|
||||||
/// [`CoreRegions::Footer`](crate::core::theme::CoreRegions::Footer) en ese orden.
|
|
||||||
///
|
///
|
||||||
/// Los temas pueden sobrescribir este método para:
|
/// Los temas pueden sobrescribir este método para:
|
||||||
///
|
///
|
||||||
|
|
@ -199,7 +198,7 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// component: &mut dyn Component,
|
/// component: &mut dyn Component,
|
||||||
/// cx: &mut Context,
|
/// cx: &mut Context,
|
||||||
/// ) -> Option<Result<Markup, ComponentError>> {
|
/// ) -> Option<Result<Markup, ComponentError>> {
|
||||||
/// // Sólo mutación: ajusta el componente y deja que otro nivel lo renderice.
|
/// // Solo mutación: ajusta el componente y deja que otro nivel lo renderice.
|
||||||
/// setup_component!(component, {
|
/// setup_component!(component, {
|
||||||
/// Button => |btn| { btn.add_class("btn-primary"); },
|
/// Button => |btn| { btn.add_class("btn-primary"); },
|
||||||
/// });
|
/// });
|
||||||
|
|
@ -222,15 +221,15 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado).
|
/// Contenido predefinido para la página de error "*403 - Forbidden*" (acceso denegado).
|
||||||
///
|
///
|
||||||
/// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo
|
/// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo
|
||||||
/// [`CoreTemplates::Standard`](crate::core::theme::CoreTemplates::Standard)), para que el
|
/// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)), para que el usuario
|
||||||
/// usuario no pierda el contexto de navegación del sitio. Los temas pueden sobrescribir este
|
/// no pierda el contexto de navegación del sitio. Los temas pueden sobrescribir este método
|
||||||
/// método para personalizar completamente el diseño y el contenido de la página de error.
|
/// para personalizar completamente el diseño y el contenido de la página de error.
|
||||||
fn error_403(&self, page: &mut Page) {
|
fn error_403(&self, page: &mut Page) {
|
||||||
if let Some(parent) = self.parent() {
|
if let Some(parent) = self.parent() {
|
||||||
return parent.error_403(page);
|
return parent.error_403(page);
|
||||||
}
|
}
|
||||||
page.alter_title(L10n::l("error403_title")).alter_child_in(
|
page.alter_title(L10n::l("error403_title")).alter_child_in(
|
||||||
&CoreRegions::Content,
|
&CoreRegion::Content,
|
||||||
ChildOp::Prepend(
|
ChildOp::Prepend(
|
||||||
Html::with(move |cx| {
|
Html::with(move |cx| {
|
||||||
html! {
|
html! {
|
||||||
|
|
@ -248,7 +247,7 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
/// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado).
|
/// Contenido predefinido para la página de error "*404 - Not Found*" (recurso no encontrado).
|
||||||
///
|
///
|
||||||
/// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo
|
/// Normalmente se renderiza con la plantilla ya activa en la página (por ejemplo
|
||||||
/// [`CoreTemplates::Standard`](crate::core::theme::CoreTemplates::Standard)). Los temas pueden
|
/// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)). Los temas pueden
|
||||||
/// sobrescribir este método para personalizar completamente el diseño y el contenido de la
|
/// sobrescribir este método para personalizar completamente el diseño y el contenido de la
|
||||||
/// página de error.
|
/// página de error.
|
||||||
fn error_404(&self, page: &mut Page) {
|
fn error_404(&self, page: &mut Page) {
|
||||||
|
|
@ -256,7 +255,7 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
return parent.error_404(page);
|
return parent.error_404(page);
|
||||||
}
|
}
|
||||||
page.alter_title(L10n::l("error404_title")).alter_child_in(
|
page.alter_title(L10n::l("error404_title")).alter_child_in(
|
||||||
&CoreRegions::Content,
|
&CoreRegion::Content,
|
||||||
ChildOp::Prepend(
|
ChildOp::Prepend(
|
||||||
Html::with(move |cx| {
|
Html::with(move |cx| {
|
||||||
html! {
|
html! {
|
||||||
|
|
@ -273,15 +272,19 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
|
|
||||||
/// Permite al tema preparar y componer una página de **error fatal controlado**.
|
/// Permite al tema preparar y componer una página de **error fatal controlado**.
|
||||||
///
|
///
|
||||||
/// Devuelve explícitamente [`ErrorPage::BadRequest`], [`ErrorPage::InternalError`],
|
/// Esta función decide explícitamente devolver
|
||||||
/// [`ErrorPage::ServiceUnavailable`] o [`ErrorPage::GatewayTimeout`], porque algo ha fallado,
|
/// [`ErrorPage::BadRequest`](crate::response::ErrorPage::BadRequest),
|
||||||
/// pero el servidor sigue activo y el tema, el renderizado y el resto de componentes funcionan
|
/// [`ErrorPage::InternalError`](crate::response::ErrorPage::InternalError),
|
||||||
/// con normalidad.
|
/// [`ErrorPage::ServiceUnavailable`](crate::response::ErrorPage::ServiceUnavailable) o
|
||||||
|
/// [`ErrorPage::GatewayTimeout`](crate::response::ErrorPage::GatewayTimeout) porque algo ha
|
||||||
|
/// fallado, pero el servidor sigue activo y el tema, el renderizado y el resto de componentes
|
||||||
|
/// funcionan con normalidad.
|
||||||
///
|
///
|
||||||
/// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya
|
/// Por defecto, asigna el título al documento (`title`), se renderiza con la plantilla ya
|
||||||
/// activa en la página (normalmente [`CoreTemplates::Standard`]) y muestra un componente
|
/// activa en la página (normalmente
|
||||||
/// [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados (`alert` y
|
/// [`CoreTemplate::Standard`](crate::core::theme::CoreTemplate::Standard)) y muestra un
|
||||||
/// `help`) como descripción del error.
|
/// componente [`Intro`] con el código HTTP del error (`code`) y los mensajes proporcionados
|
||||||
|
/// (`alert` y `help`) como descripción del error.
|
||||||
///
|
///
|
||||||
/// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni
|
/// Este método no se utiliza en las implementaciones predefinidas de [`Self::error_403()`] ni
|
||||||
/// [`Self::error_404()`], que definen su propio contenido específico.
|
/// [`Self::error_404()`], que definen su propio contenido específico.
|
||||||
|
|
@ -293,18 +296,12 @@ pub trait Theme: Extension + Send + Sync {
|
||||||
///
|
///
|
||||||
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
|
/// Los temas pueden sobrescribir este método para personalizar el diseño y el contenido de la
|
||||||
/// página de error.
|
/// página de error.
|
||||||
///
|
|
||||||
/// [`ErrorPage::BadRequest`]: crate::response::ErrorPage::BadRequest
|
|
||||||
/// [`ErrorPage::InternalError`]: crate::response::ErrorPage::InternalError
|
|
||||||
/// [`ErrorPage::ServiceUnavailable`]: crate::response::ErrorPage::ServiceUnavailable
|
|
||||||
/// [`ErrorPage::GatewayTimeout`]: crate::response::ErrorPage::GatewayTimeout
|
|
||||||
/// [`CoreTemplates::Standard`]: crate::core::theme::CoreTemplates::Standard
|
|
||||||
fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) {
|
fn error_fatal(&self, page: &mut Page, code: StatusCode, title: L10n, alert: L10n, help: L10n) {
|
||||||
if let Some(parent) = self.parent() {
|
if let Some(parent) = self.parent() {
|
||||||
return parent.error_fatal(page, code, title, alert, help);
|
return parent.error_fatal(page, code, title, alert, help);
|
||||||
}
|
}
|
||||||
page.alter_title(title).alter_child_in(
|
page.alter_title(title).alter_child_in(
|
||||||
&CoreRegions::Content,
|
&CoreRegion::Content,
|
||||||
ChildOp::Prepend(
|
ChildOp::Prepend(
|
||||||
Intro::new()
|
Intro::new()
|
||||||
.with_title(L10n::l("error_code").with_arg("code", code.to_string()))
|
.with_title(L10n::l("error_code").with_arg("code", code.to_string()))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::core::component::{Child, ChildOp, Children, Component};
|
use crate::core::component::{Child, ChildOp, Children, Component};
|
||||||
use crate::core::theme::{CoreRegions, RegionRef, ThemeRef};
|
use crate::core::theme::{CoreRegion, RegionRef, ThemeRef};
|
||||||
use crate::{AutoDefault, UniqueId, builder_fn};
|
use crate::{AutoDefault, UniqueId, builder_fn};
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
@ -132,13 +132,13 @@ impl ChildrenInRegions {
|
||||||
/// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" }));
|
/// InRegion::Content.add(Html::with(|_| html! { "🎉 ¡Bienvenido!" }));
|
||||||
///
|
///
|
||||||
/// // Texto en la cabecera, visible en todos los temas.
|
/// // Texto en la cabecera, visible en todos los temas.
|
||||||
/// InRegion::Global(&CoreRegions::Header).add(Html::with(|_| html! { "Publicidad" }));
|
/// InRegion::Global(&CoreRegion::Header).add(Html::with(|_| html! { "Publicidad" }));
|
||||||
/// ```
|
/// ```
|
||||||
pub enum InRegion {
|
pub enum InRegion {
|
||||||
/// Región principal de **contenido** por defecto.
|
/// Región principal de **contenido** por defecto.
|
||||||
///
|
///
|
||||||
/// Añade el componente a la región lógica de contenido principal de la aplicación. Internamente
|
/// Añade el componente a la región lógica de contenido principal de la aplicación. Internamente
|
||||||
/// equivale a `InRegion::Global(&CoreRegions::Content)`.
|
/// equivale a `InRegion::Global(&CoreRegion::Content)`.
|
||||||
Content,
|
Content,
|
||||||
/// Región global compartida por todos los temas.
|
/// Región global compartida por todos los temas.
|
||||||
///
|
///
|
||||||
|
|
@ -173,19 +173,19 @@ impl InRegion {
|
||||||
/// }));
|
/// }));
|
||||||
///
|
///
|
||||||
/// // Texto en la cabecera.
|
/// // Texto en la cabecera.
|
||||||
/// InRegion::Global(&CoreRegions::Header).add(Html::with(|_| {
|
/// InRegion::Global(&CoreRegion::Header).add(Html::with(|_| {
|
||||||
/// html! { "Publicidad" }
|
/// html! { "Publicidad" }
|
||||||
/// }));
|
/// }));
|
||||||
///
|
///
|
||||||
/// // Contenido sólo para la región del pie de página en un tema concreto.
|
/// // Contenido sólo para la región del pie de página en un tema concreto.
|
||||||
/// InRegion::ForTheme(&theme::Basic, &CoreRegions::Footer).add(Html::with(|_| {
|
/// InRegion::ForTheme(&theme::Basic, &CoreRegion::Footer).add(Html::with(|_| {
|
||||||
/// html! { "Aviso legal" }
|
/// html! { "Aviso legal" }
|
||||||
/// }));
|
/// }));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn add(&self, component: impl Component) -> &Self {
|
pub fn add(&self, component: impl Component) -> &Self {
|
||||||
let proto: Arc<dyn Component> = Arc::new(component);
|
let proto: Arc<dyn Component> = Arc::new(component);
|
||||||
match self {
|
match self {
|
||||||
InRegion::Content => Self::add_to_common(&CoreRegions::Content, proto),
|
InRegion::Content => Self::add_to_common(&CoreRegion::Content, proto),
|
||||||
InRegion::Global(region) => Self::add_to_common(*region, proto),
|
InRegion::Global(region) => Self::add_to_common(*region, proto),
|
||||||
InRegion::ForTheme(theme, region) => {
|
InRegion::ForTheme(theme, region) => {
|
||||||
THEME_REGIONS
|
THEME_REGIONS
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
//! composición del `<head>` y del `<body>`, y se ejecutan las acciones registradas por las
|
//! composición del `<head>` y del `<body>`, y se ejecutan las acciones registradas por las
|
||||||
//! extensiones antes y después de generar los contenidos.
|
//! extensiones antes y después de generar los contenidos.
|
||||||
//!
|
//!
|
||||||
//! También define las regiones internas reservadas ([`ReservedRegions`]) que actúan como puntos de
|
//! También define las regiones internas reservadas ([`ReservedRegion`]) que actúan como puntos de
|
||||||
//! anclaje globales al inicio y al final del `<body>`, fuera de las regiones que maqueta la
|
//! anclaje globales al inicio y al final del `<body>`, fuera de las regiones que maqueta la
|
||||||
//! plantilla activa.
|
//! plantilla activa.
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ use crate::base::action;
|
||||||
use crate::base::component::layout;
|
use crate::base::component::layout;
|
||||||
use crate::core::component::{AssetsOp, ChildOp, ComponentRender};
|
use crate::core::component::{AssetsOp, ChildOp, ComponentRender};
|
||||||
use crate::core::component::{Context, ContextError, Contextual};
|
use crate::core::component::{Context, ContextError, Contextual};
|
||||||
use crate::core::theme::{CoreRegions, RegionName, RegionRef, TemplateRef, ThemeRef};
|
use crate::core::theme::{CoreRegion, RegionName, RegionRef, TemplateRef, ThemeRef};
|
||||||
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
use crate::html::{Assets, Favicon, JavaScript, StyleSheet};
|
||||||
use crate::html::{Attr, Props, PropsOp};
|
use crate::html::{Attr, Props, PropsOp};
|
||||||
use crate::html::{DOCTYPE, Markup, html};
|
use crate::html::{DOCTYPE, Markup, html};
|
||||||
|
|
@ -31,7 +31,7 @@ use crate::locale::{CharacterDirection, L10n, LangId, LanguageIdentifier};
|
||||||
use crate::web::HttpRequest;
|
use crate::web::HttpRequest;
|
||||||
use crate::{AutoDefault, builder_fn};
|
use crate::{AutoDefault, builder_fn};
|
||||||
|
|
||||||
// **< ReservedRegions >****************************************************************************
|
// **< ReservedRegion >*****************************************************************************
|
||||||
|
|
||||||
/// Regiones internas reservadas como puntos de anclaje globales.
|
/// Regiones internas reservadas como puntos de anclaje globales.
|
||||||
///
|
///
|
||||||
|
|
@ -41,7 +41,7 @@ use crate::{AutoDefault, builder_fn};
|
||||||
/// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body). **No suelen usarse
|
/// [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body). **No suelen usarse
|
||||||
/// como regiones "visibles" en los temas**, sino para inyectar contenido global o técnico.
|
/// como regiones "visibles" en los temas**, sino para inyectar contenido global o técnico.
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub enum ReservedRegions {
|
pub enum ReservedRegion {
|
||||||
/// Región interna situada al **inicio del `<body>`**, de nombre `"page-top"`.
|
/// Región interna situada al **inicio del `<body>`**, de nombre `"page-top"`.
|
||||||
///
|
///
|
||||||
/// Proporciona un contenedor donde las extensiones puedan inyectar elementos auxiliares antes
|
/// Proporciona un contenedor donde las extensiones puedan inyectar elementos auxiliares antes
|
||||||
|
|
@ -63,7 +63,7 @@ pub enum ReservedRegions {
|
||||||
PageBottom,
|
PageBottom,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RegionName for ReservedRegions {
|
impl RegionName for ReservedRegion {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -109,13 +109,13 @@ impl Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea una nueva instancia de página con la plantilla [`CoreTemplates::Admin`].
|
/// Crea una nueva instancia de página con la plantilla [`CoreTemplate::Admin`].
|
||||||
///
|
///
|
||||||
/// Cada tema puede maquetarla de forma distinta capturando
|
/// Cada tema puede maquetarla de forma distinta capturando
|
||||||
/// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la
|
/// [`Template`](crate::base::component::layout::Template) en `handle_component()`, pero la
|
||||||
/// plantilla en sí es la misma constante para cualquier tema.
|
/// plantilla en sí es la misma constante para cualquier tema.
|
||||||
///
|
///
|
||||||
/// [`CoreTemplates::Admin`]: crate::core::theme::CoreTemplates::Admin
|
/// [`CoreTemplate::Admin`]: crate::core::theme::CoreTemplate::Admin
|
||||||
pub fn admin(request: HttpRequest) -> Self {
|
pub fn admin(request: HttpRequest) -> Self {
|
||||||
Page {
|
Page {
|
||||||
context: Context::admin(request),
|
context: Context::admin(request),
|
||||||
|
|
@ -196,10 +196,10 @@ impl Page {
|
||||||
/// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar
|
/// 2. Despacha [`action::page::BeforeRenderBody`] para que otras extensiones puedan realizar
|
||||||
/// ajustes previos sobre la página.
|
/// ajustes previos sobre la página.
|
||||||
/// 3. **Construye el contenido del `<body>`**:
|
/// 3. **Construye el contenido del `<body>`**:
|
||||||
/// - Renderiza la región reservada superior ([`ReservedRegions::PageTop`]).
|
/// - Renderiza la región reservada superior ([`ReservedRegion::PageTop`]).
|
||||||
/// - Llama a [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para
|
/// - Llama a [`Theme::render_page_body()`](crate::core::theme::Theme::render_page_body) para
|
||||||
/// renderizar las regiones del cuerpo principal de la página.
|
/// renderizar las regiones del cuerpo principal de la página.
|
||||||
/// - Renderiza la región reservada inferior ([`ReservedRegions::PageBottom`]).
|
/// - Renderiza la región reservada inferior ([`ReservedRegion::PageBottom`]).
|
||||||
/// 4. Ejecuta
|
/// 4. Ejecuta
|
||||||
/// [`Theme::after_render_page_body()`](crate::core::theme::Theme::after_render_page_body)
|
/// [`Theme::after_render_page_body()`](crate::core::theme::Theme::after_render_page_body)
|
||||||
/// para que el tema pueda aplicar ajustes finales.
|
/// para que el tema pueda aplicar ajustes finales.
|
||||||
|
|
@ -221,9 +221,9 @@ impl Page {
|
||||||
|
|
||||||
// Renderiza el <body>.
|
// Renderiza el <body>.
|
||||||
let body = html! {
|
let body = html! {
|
||||||
(layout::Region::of(&ReservedRegions::PageTop).render(&mut self.context).await)
|
(layout::Region::of(&ReservedRegion::PageTop).render(&mut self.context).await)
|
||||||
(self.context.theme().render_page_body(self).await)
|
(self.context.theme().render_page_body(self).await)
|
||||||
(layout::Region::of(&ReservedRegions::PageBottom).render(&mut self.context).await)
|
(layout::Region::of(&ReservedRegion::PageBottom).render(&mut self.context).await)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Acciones específicas del tema después de renderizar el <body>.
|
// Acciones específicas del tema después de renderizar el <body>.
|
||||||
|
|
@ -314,8 +314,7 @@ impl Contextual for Page {
|
||||||
|
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||||
self.context
|
self.context.alter_child_in(&CoreRegion::Content, op.into());
|
||||||
.alter_child_in(&CoreRegions::Content, op.into());
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ async fn setup() {
|
||||||
// **< A theme that intercepts the `Template` component >*******************************************
|
// **< A theme that intercepts the `Template` component >*******************************************
|
||||||
|
|
||||||
/// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed
|
/// Replaces the default `Template` composition (`Header` + `Content` + `Footer`) with a fixed
|
||||||
/// marker string, for both `CoreTemplates::Standard` and `CoreTemplates::Admin`. Mirrors how
|
/// marker string, for both `CoreTemplate::Standard` and `CoreTemplate::Admin`. Mirrors how
|
||||||
/// a real theme (e.g. `pagetop-bootsier`) tells its own layout apart from PageTop's default: by
|
/// a real theme (e.g. `pagetop-bootsier`) tells its own layout apart from PageTop's default: by
|
||||||
/// intercepting the `Template` component in `handle_component()`, not by swapping which
|
/// intercepting the `Template` component in `handle_component()`, not by swapping which
|
||||||
/// `TemplateRef` gets resolved.
|
/// `TemplateRef` gets resolved.
|
||||||
|
|
@ -32,7 +32,7 @@ impl Theme for MarkerTheme {
|
||||||
_cx: &mut Context,
|
_cx: &mut Context,
|
||||||
) -> Option<Result<Markup, ComponentError>> {
|
) -> Option<Result<Markup, ComponentError>> {
|
||||||
let template = (&*component).downcast_ref::<layout::Template>()?;
|
let template = (&*component).downcast_ref::<layout::Template>()?;
|
||||||
template.template().downcast_ref::<CoreTemplates>()?;
|
template.template().downcast_ref::<CoreTemplate>()?;
|
||||||
Some(Ok(html! { "marker-template-output" }))
|
Some(Ok(html! { "marker-template-output" }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ impl Theme for MarkerTheme {
|
||||||
// **< Default/Admin template identity is independent of the active theme >*************************
|
// **< Default/Admin template identity is independent of the active theme >*************************
|
||||||
//
|
//
|
||||||
// `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always
|
// `Theme::default_template()`/`admin_template()` were removed: `Context::template()` always
|
||||||
// resolves `Default`/`Admin` to the core `CoreTemplates::Standard`/`Admin` identity, regardless
|
// resolves `Default`/`Admin` to the core `CoreTemplate::Standard`/`Admin` identity, regardless
|
||||||
// of which theme is active. Themes customize the actual rendering by intercepting the `Template`
|
// of which theme is active. Themes customize the actual rendering by intercepting the `Template`
|
||||||
// component in `handle_component()` instead (see the tests further below).
|
// component in `handle_component()` instead (see the tests further below).
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ async fn explicit_template_is_not_overridden_by_a_later_with_theme() {
|
||||||
// A template explicitly set with `with_template()` prevails even if `with_theme()` is called
|
// A template explicitly set with `with_template()` prevails even if `with_theme()` is called
|
||||||
// afterwards.
|
// afterwards.
|
||||||
let cx = Context::default()
|
let cx = Context::default()
|
||||||
.with_template(&CoreTemplates::Admin)
|
.with_template(&CoreTemplate::Admin)
|
||||||
.with_theme(&pagetop::base::theme::Basic);
|
.with_theme(&pagetop::base::theme::Basic);
|
||||||
|
|
||||||
assert_eq!(cx.template().name(), "admin");
|
assert_eq!(cx.template().name(), "admin");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue