Compare commits
No commits in common. "825283caa339b21200c84264aa5637d9897ffadb" and "3b01894024610481cd9bb925b5e27bb6e6c68d79" have entirely different histories.
825283caa3
...
3b01894024
6 changed files with 37 additions and 510 deletions
|
|
@ -137,145 +137,6 @@ input:disabled + label {
|
||||||
color: var(--val-color--text--muted);
|
color: var(--val-color--text--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Pager component
|
|
||||||
*/
|
|
||||||
|
|
||||||
.pager {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin: 1rem 0;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
.pager-align-start {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
.pager-align-center {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.pager-align-end {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
.pager-summary {
|
|
||||||
color: var(--val-color--text--muted);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
align-self: flex-start;
|
|
||||||
margin-inline-end: auto;
|
|
||||||
}
|
|
||||||
.pager-align-start .pager-summary {
|
|
||||||
order: 1;
|
|
||||||
margin-inline-end: 0;
|
|
||||||
margin-inline-start: auto;
|
|
||||||
}
|
|
||||||
.pager-align-center .pager-summary {
|
|
||||||
order: 1;
|
|
||||||
flex-basis: 100%;
|
|
||||||
text-align: center;
|
|
||||||
margin-inline-end: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.pagination a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 1.5rem;
|
|
||||||
padding: 0.375rem 0.625rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
color: var(--val-color--text);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.pagination a:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--val-color--text) 8%, transparent);
|
|
||||||
}
|
|
||||||
.pagination .active a {
|
|
||||||
color: #fff;
|
|
||||||
background-color: var(--val-color--primary);
|
|
||||||
}
|
|
||||||
.pagination .disabled a {
|
|
||||||
color: var(--val-color--text--muted);
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-ellipsis {
|
|
||||||
padding: 0.375rem 0.25rem;
|
|
||||||
color: var(--val-color--text--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .page-link-icon muestra el texto de navegación de los botones anterior/siguiente. El texto
|
|
||||||
accesible completo va aparte, en el aria-label del enlace. Por eso sustituir el texto visible por
|
|
||||||
otro contenido no afecta a la accesibilidad, p.ej. para usar "‹"/"›" se puede aplicar:
|
|
||||||
|
|
||||||
.page-link-icon {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0;
|
|
||||||
}
|
|
||||||
.page-previous .page-link-icon::before {
|
|
||||||
content: "‹";
|
|
||||||
font-size: 1rem;
|
|
||||||
display: inline-block;
|
|
||||||
transform: translateY(-0.15em) scale(1.5);
|
|
||||||
}
|
|
||||||
.page-next .page-link-icon::before {
|
|
||||||
content: "›";
|
|
||||||
font-size: 1rem;
|
|
||||||
display: inline-block;
|
|
||||||
transform: translateY(-0.15em) scale(1.5);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
.pager-jump {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.pager-jump:focus-within {
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--val-color--primary) 25%, transparent);
|
|
||||||
}
|
|
||||||
.pager-jump-input > input.form-control:focus,
|
|
||||||
.pager-jump-button:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
.pager-jump-input > input.form-control {
|
|
||||||
width: calc(var(--pager-jump-width, 1ch) + 2.5rem);
|
|
||||||
padding: 0.375rem 0.625rem;
|
|
||||||
line-height: var(--val-lh--base);
|
|
||||||
border: 0;
|
|
||||||
border-start-end-radius: 0;
|
|
||||||
border-end-end-radius: 0;
|
|
||||||
background-color: var(--val-color--light);
|
|
||||||
}
|
|
||||||
.pager-jump-input > input.form-control {
|
|
||||||
text-align: center;
|
|
||||||
appearance: textfield;
|
|
||||||
-moz-appearance: textfield;
|
|
||||||
}
|
|
||||||
.pager-jump-input > input.form-control::-webkit-outer-spin-button,
|
|
||||||
.pager-jump-input > input.form-control::-webkit-inner-spin-button {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.pager-jump-button {
|
|
||||||
padding: 0.375rem 0.625rem;
|
|
||||||
border: 0;
|
|
||||||
border-start-start-radius: 0;
|
|
||||||
border-end-start-radius: 0;
|
|
||||||
}
|
|
||||||
.pager-jump-button:hover {
|
|
||||||
background-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Region Footer
|
* Region Footer
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,6 @@ pub struct Button {
|
||||||
value: AttrValue,
|
value: AttrValue,
|
||||||
/// Devuelve la etiqueta del botón.
|
/// Devuelve la etiqueta del botón.
|
||||||
label: Attr<L10n>,
|
label: Attr<L10n>,
|
||||||
/// Devuelve el texto emergente del botón (atributo `title`).
|
|
||||||
title: Attr<L10n>,
|
|
||||||
/// Devuelve si el botón recibe el foco automáticamente al cargar la página.
|
/// Devuelve si el botón recibe el foco automáticamente al cargar la página.
|
||||||
autofocus: bool,
|
autofocus: bool,
|
||||||
/// Devuelve si el botón está deshabilitado.
|
/// Devuelve si el botón está deshabilitado.
|
||||||
|
|
@ -104,7 +102,6 @@ impl Component for Button {
|
||||||
(self.props())
|
(self.props())
|
||||||
name=[self.name().get()]
|
name=[self.name().get()]
|
||||||
value=[self.value().get()]
|
value=[self.value().get()]
|
||||||
title=[self.title().lookup(cx)]
|
|
||||||
autofocus[*self.autofocus()]
|
autofocus[*self.autofocus()]
|
||||||
disabled[*self.disabled()]
|
disabled[*self.disabled()]
|
||||||
{
|
{
|
||||||
|
|
@ -195,13 +192,6 @@ impl Button {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece o elimina el texto emergente del botón (basta pasar `None` para quitarlo).
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_title(mut self, title: impl Into<Option<L10n>>) -> Self {
|
|
||||||
self.title.alter_opt(title.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece si el botón recibe el foco automáticamente al cargar la página.
|
/// Establece si el botón recibe el foco automáticamente al cargar la página.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -14,24 +14,13 @@ pub enum PagerVisibility {
|
||||||
Auto,
|
Auto,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Define la alineación horizontal de [`Pager`] dentro de su contenedor.
|
|
||||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub enum PagerAlign {
|
|
||||||
/// Alineado al comienzo.
|
|
||||||
Start,
|
|
||||||
/// Centrado (comportamiento por defecto).
|
|
||||||
#[default]
|
|
||||||
Center,
|
|
||||||
/// Alineado al final.
|
|
||||||
End,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Componente para añadir un **paginador** a un listado.
|
/// Componente para añadir un **paginador** a un listado.
|
||||||
///
|
///
|
||||||
/// `Pager` permite navegar por las páginas de un listado de ítems cuando supera el número máximo de
|
/// `Pager` permite navegar por las páginas de un listado de ítems cuando supera el número máximo de
|
||||||
/// ítems admitidos por página. Resuelve el enlace para acceder a cada página del listado a partir
|
/// ítems admitidos por página. Resuelve el enlace para acceder a cada página del listado a partir
|
||||||
/// de una ruta base, un conjunto de parámetros de consulta adicionales (orden, búsqueda, etc.) y el
|
/// de una ruta base, un conjunto de parámetros de consulta adicionales (orden, búsqueda, etc.) y el
|
||||||
/// estado actual ([`current_page`], [`items_per_page`], [`total_items`]).
|
/// estado actual ([`current_page`](Self::current_page), [`items_per_page`](Self::items_per_page),
|
||||||
|
/// [`total_items`](Self::total_items)).
|
||||||
///
|
///
|
||||||
/// El componente se renderiza sólo si el listado requiere más de una página.
|
/// El componente se renderiza sólo si el listado requiere más de una página.
|
||||||
///
|
///
|
||||||
|
|
@ -40,31 +29,28 @@ pub enum PagerAlign {
|
||||||
///
|
///
|
||||||
/// El listado de páginas se flanquea con dos botones de navegación: página anterior y página
|
/// El listado de páginas se flanquea con dos botones de navegación: página anterior y página
|
||||||
/// siguiente (las páginas primera y última ya están siempre disponibles como números, así que no
|
/// siguiente (las páginas primera y última ya están siempre disponibles como números, así que no
|
||||||
/// llevan un botón dedicado). Su visibilidad, junto a la del formulario de salto a página y la del
|
/// llevan un botón dedicado). Su visibilidad, junto a la del formulario de salto a página, se
|
||||||
/// resumen de páginas, se controla con [`PagerVisibility`] a través de [`with_prev_next()`],
|
/// controla con [`PagerVisibility`] a través de [`with_prev_next()`](Self::with_prev_next) y
|
||||||
/// [`with_jump()`] y [`with_summary()`]: `Never` los oculta siempre, `Always` los muestra siempre,
|
/// [`with_jump()`](Self::with_jump): `Never` los oculta siempre, `Always` los muestra siempre (en
|
||||||
/// y el valor por defecto `Auto` los muestra sólo cuando el paginador necesita truncar el listado
|
/// el caso de los botones, con el extremo correspondiente desactivado en vez de oculto), y `Auto`
|
||||||
/// de páginas por superar el número máximo de páginas visible.
|
/// -el valor por defecto de ambos- los muestra sólo cuando el número total de páginas supera al
|
||||||
|
/// número de páginas que se muestra en el paginador; en ese caso, si la página actual coincide con
|
||||||
|
/// un extremo, el botón correspondiente se muestra igualmente, pero desactivado.
|
||||||
///
|
///
|
||||||
/// Un elemento `<nav>` envuelve todo el paginador. Lleva un `aria-label` por defecto que puede
|
/// Un elemento `<nav>` envuelve todo el paginador. Lleva un `aria-label` por defecto que puede
|
||||||
/// sustituirse por otro más específico usando [`with_aria_label()`], por ejemplo cuando una misma
|
/// sustituirse con [`with_aria_label()`](Self::with_aria_label) por otro más específico, por
|
||||||
/// página tiene varios paginadores.
|
/// ejemplo cuando una misma página tiene varios paginadores.
|
||||||
///
|
|
||||||
/// La alineación horizontal del paginador dentro de este contenedor se controla con [`PagerAlign`]
|
|
||||||
/// a través de [`with_align()`]. Por defecto es [`PagerAlign::Center`].
|
|
||||||
///
|
///
|
||||||
/// # Acotando el número de ítems del paginador
|
/// # Acotando el número de ítems del paginador
|
||||||
///
|
///
|
||||||
/// Con listados largos, mostrar un número por cada página real puede desbordar la interfaz. Con
|
/// Con listados largos, mostrar un número por cada página real puede desbordar la interfaz. Con
|
||||||
/// [`with_window()`] se puede limitar el número de páginas que se muestran a cada lado de la página
|
/// [`with_window()`](Self::with_window) se puede limitar el número de páginas que se muestran a
|
||||||
/// actual. Por defecto vale `2`, que limita la vista a `9` celdas en total (sin contar los botones
|
/// cada lado de la página actual. Por defecto vale `2` para no mostrar más de 9 celdas en total.
|
||||||
/// de navegación anterior/siguiente). En general, el número máximo de celdas mostradas para un
|
|
||||||
/// `window` dado es `2 * window + 5`.
|
|
||||||
///
|
///
|
||||||
/// Incluso cuando se trunca el paginador, `Pager` siempre muestra la primera y la última página
|
/// Si el valor de la ventana es mayor que `0`, `Pager` siempre muestra la primera y la última
|
||||||
/// como números, más la ventana indicada antes y después de la página actual, sustituyendo por una
|
/// página como números, más la ventana indicada antes y después de la página actual, sustituyendo
|
||||||
/// elipsis (`…`) cualquier tramo oculto de dos o más páginas. Si el tramo oculto es de una sola
|
/// por una elipsis (`…`) cualquier tramo oculto de dos o más páginas. Si el tramo oculto es de una
|
||||||
/// página, se muestra directamente en vez de la elipsis, porque ocultarla no ahorra espacio.
|
/// sola página, se muestra directamente en vez de la elipsis, porque ocultarla no ahorra espacio.
|
||||||
///
|
///
|
||||||
/// Por ejemplo, con `with_window(3)`, página actual `34` y con `200` páginas en total, el paginador
|
/// Por ejemplo, con `with_window(3)`, página actual `34` y con `200` páginas en total, el paginador
|
||||||
/// se mostraría así:
|
/// se mostraría así:
|
||||||
|
|
@ -73,31 +59,22 @@ pub enum PagerAlign {
|
||||||
/// ‹ | 1 | … | 31 | 32 | 33 | [34] | 35 | 36 | 37 | … | 200 | ›
|
/// ‹ | 1 | … | 31 | 32 | 33 | [34] | 35 | 36 | 37 | … | 200 | ›
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Cuando la página actual está cerca de los extremos, se ajustan las páginas numeradas para
|
|
||||||
/// mantener el número de celdas mostradas según el valor de `window`.
|
|
||||||
///
|
|
||||||
/// Cuando corresponda según [`jump()`](Self::jump), [`Pager`] puede añadir un pequeño formulario
|
/// Cuando corresponda según [`jump()`](Self::jump), [`Pager`] puede añadir un pequeño formulario
|
||||||
/// para saltar directamente a una página escribiendo su número, sin depender de JavaScript. Un
|
/// para saltar directamente a una página escribiendo su número, sin depender de JavaScript. Un
|
||||||
/// único campo numérico (`min`/`max` según el total de páginas) y un botón de envío.
|
/// único campo numérico (`min`/`max` según el total de páginas) y un botón de envío.
|
||||||
///
|
///
|
||||||
/// Con [`with_summary()`] se puede añadir un texto que resume la vista de las páginas que se
|
|
||||||
/// muestran en ese momento (ver más arriba su visibilidad según [`PagerVisibility`]).
|
|
||||||
///
|
|
||||||
/// # Clases CSS
|
/// # Clases CSS
|
||||||
///
|
///
|
||||||
/// - `.pager` - clase base del componente (elemento `<nav>`).
|
/// - `.pager` - clase base del componente (elemento `<nav>`).
|
||||||
/// - `.pager-align-start` / `.pager-align-center` / `.pager-align-end` - según [`PagerAlign`].
|
|
||||||
/// - `.pager-summary` - clase del resumen de las páginas mostradas, si está activado.
|
|
||||||
/// - `.pagination` - clase del elemento `<ul>` que contiene los enlaces de página.
|
/// - `.pagination` - clase del elemento `<ul>` que contiene los enlaces de página.
|
||||||
/// - `.page-item` - presente en todos los `<li>` del listado.
|
/// - `.page-item` - presente en todos los `<li>` del listado.
|
||||||
/// - `.page-link` - presente en todos los enlaces (`<a>`) del listado, y también en el `<span>` de
|
/// - `.page-link` - presente en todos los enlaces (`<a>`) del listado.
|
||||||
/// la elipsis, para que comparta con ellos el aspecto de celda (borde, fondo, radio, margen).
|
/// - `.page-link-icon` - envuelve el carácter (`‹`/`›`) de los botones de navegación, para poder
|
||||||
/// - `.page-link-icon` - envuelve el texto de los botones de navegación anterior/siguiente.
|
/// ajustar su tamaño o posición sin afectar al área interactiva de `.page-link`.
|
||||||
/// - `.page-previous` / `.page-next` - añadidas a los `<li>` de página anterior/siguiente.
|
/// - `.page-previous` / `.page-next` - añadidas a los `<li>` de página anterior/siguiente.
|
||||||
/// - `.page-ellipsis` - clase del `<li>` que representa un tramo de páginas ocultas.
|
/// - `.page-ellipsis` - clase del `<li>` que representa un tramo de páginas ocultas.
|
||||||
/// - `.active` - añadida al `<li>` de la página actualmente visible.
|
/// - `.active` - añadida al `<li>` de la página actualmente visible.
|
||||||
/// - `.disabled` - añadida al `<li>` de los extremos cuando no procede navegar, y también al de la
|
/// - `.disabled` - añadida al `<li>` de los extremos cuando no procede navegar.
|
||||||
/// elipsis, ya que tampoco es interactiva.
|
|
||||||
/// - `.pager-jump` - clase del `<form>` para saltar directamente a una página.
|
/// - `.pager-jump` - clase del `<form>` para saltar directamente a una página.
|
||||||
/// - `.pager-jump-input` - clase del campo numérico del formulario de salto.
|
/// - `.pager-jump-input` - clase del campo numérico del formulario de salto.
|
||||||
/// - `.pager-jump-button` - clase del botón de envío del formulario de salto.
|
/// - `.pager-jump-button` - clase del botón de envío del formulario de salto.
|
||||||
|
|
@ -119,16 +96,6 @@ pub enum PagerAlign {
|
||||||
/// .with_items_per_page(20)
|
/// .with_items_per_page(20)
|
||||||
/// .with_total_items(97);
|
/// .with_total_items(97);
|
||||||
/// ```
|
/// ```
|
||||||
///
|
|
||||||
/// [`current_page`]: Self::current_page
|
|
||||||
/// [`items_per_page`]: Self::items_per_page
|
|
||||||
/// [`total_items`]: Self::total_items
|
|
||||||
/// [`with_window()`]: Self::with_window
|
|
||||||
/// [`with_align()`]: Self::with_align
|
|
||||||
/// [`with_summary()`]: Self::with_summary
|
|
||||||
/// [`with_prev_next()`]: Self::with_prev_next
|
|
||||||
/// [`with_jump()`]: Self::with_jump
|
|
||||||
/// [`with_aria_label()`]: Self::with_aria_label
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub struct Pager {
|
pub struct Pager {
|
||||||
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
/// Devuelve identificador, clases CSS, atributos HTML y valores extra del componente.
|
||||||
|
|
@ -151,10 +118,6 @@ pub struct Pager {
|
||||||
/// con un número pequeño de páginas.
|
/// con un número pequeño de páginas.
|
||||||
#[default(2)]
|
#[default(2)]
|
||||||
window: u64,
|
window: u64,
|
||||||
/// Devuelve la alineación horizontal del paginador dentro de su contenedor.
|
|
||||||
align: PagerAlign,
|
|
||||||
/// Devuelve la visibilidad del resumen de las páginas que se muestran en cada vista.
|
|
||||||
summary: PagerVisibility,
|
|
||||||
/// Devuelve la visibilidad de los botones de página anterior/siguiente.
|
/// Devuelve la visibilidad de los botones de página anterior/siguiente.
|
||||||
prev_next: PagerVisibility,
|
prev_next: PagerVisibility,
|
||||||
/// Devuelve la visibilidad del formulario para saltar directamente a una página.
|
/// Devuelve la visibilidad del formulario para saltar directamente a una página.
|
||||||
|
|
@ -187,11 +150,6 @@ impl Component for Pager {
|
||||||
let id = cx.required_id::<Self>(self.id(), 1);
|
let id = cx.required_id::<Self>(self.id(), 1);
|
||||||
self.alter_prop(PropsOp::ensure_id(id));
|
self.alter_prop(PropsOp::ensure_id(id));
|
||||||
self.alter_prop(PropsOp::prepend_classes("pager"));
|
self.alter_prop(PropsOp::prepend_classes("pager"));
|
||||||
self.alter_prop(PropsOp::add_classes(match self.align() {
|
|
||||||
PagerAlign::Start => "pager-align-start",
|
|
||||||
PagerAlign::Center => "pager-align-center",
|
|
||||||
PagerAlign::End => "pager-align-end",
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
|
|
@ -225,34 +183,17 @@ impl Component for Pager {
|
||||||
PagerVisibility::Always => true,
|
PagerVisibility::Always => true,
|
||||||
PagerVisibility::Auto => truncated,
|
PagerVisibility::Auto => truncated,
|
||||||
};
|
};
|
||||||
let show_summary = match self.summary() {
|
|
||||||
PagerVisibility::Never => false,
|
|
||||||
PagerVisibility::Always => true,
|
|
||||||
PagerVisibility::Auto => truncated,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
nav (self.props()) aria-label=(self.aria_label().using(cx)) {
|
nav (self.props()) aria-label=(self.aria_label().using(cx)) {
|
||||||
@if show_summary {
|
|
||||||
@let items_per_page = self.items_per_page().max(1);
|
|
||||||
@let first = (page - 1) * items_per_page + 1;
|
|
||||||
@let last = (page * items_per_page).min(self.total_items());
|
|
||||||
span.pager-summary {
|
|
||||||
(L10n::l("pager_summary")
|
|
||||||
.with_arg("first", first.to_string())
|
|
||||||
.with_arg("last", last.to_string())
|
|
||||||
.with_arg("total", self.total_items().to_string())
|
|
||||||
.using(cx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ul.pagination {
|
ul.pagination {
|
||||||
@if show_prev_next {
|
@if show_prev_next {
|
||||||
li.page-item.page-previous.disabled[first_disabled] {
|
li.page-item.page-previous.disabled[first_disabled] {
|
||||||
a.page-link
|
a.page-link
|
||||||
href=[(!first_disabled).then(|| Self::page_route(&route, page - 1))]
|
href=[(!first_disabled).then(|| Self::page_route(&route, page - 1))]
|
||||||
aria-disabled=[first_disabled.then_some("true")]
|
aria-disabled=[first_disabled.then_some("true")]
|
||||||
aria-label=(L10n::l("pager_previous_aria_label").using(cx)) {
|
aria-label=(L10n::l("pager_previous_label").using(cx)) {
|
||||||
span.page-link-icon { (L10n::l("pager_previous_label").using(cx)) }
|
span.page-link-icon { "‹" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -269,9 +210,7 @@ impl Component for Pager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PageItem::Ellipsis => {
|
PageItem::Ellipsis => {
|
||||||
li.page-item.page-ellipsis.disabled aria-hidden="true" {
|
li.page-item.page-ellipsis aria-hidden="true" { "…" }
|
||||||
span.page-link { "…" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -280,8 +219,8 @@ impl Component for Pager {
|
||||||
a.page-link
|
a.page-link
|
||||||
href=[(!last_disabled).then(|| Self::page_route(&route, page + 1))]
|
href=[(!last_disabled).then(|| Self::page_route(&route, page + 1))]
|
||||||
aria-disabled=[last_disabled.then_some("true")]
|
aria-disabled=[last_disabled.then_some("true")]
|
||||||
aria-label=(L10n::l("pager_next_aria_label").using(cx)) {
|
aria-label=(L10n::l("pager_next_label").using(cx)) {
|
||||||
span.page-link-icon { (L10n::l("pager_next_label").using(cx)) }
|
span.page-link-icon { "›" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -307,23 +246,19 @@ impl Component for Pager {
|
||||||
form = form.with_child(form::Hidden::field("lang", lang));
|
form = form.with_child(form::Hidden::field("lang", lang));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info para ajustar el ancho del campo al número de dígitos de `total_pages`.
|
|
||||||
let jump_width = util::join!(&total_pages.to_string().len().to_string(), "ch");
|
|
||||||
|
|
||||||
form.with_child(
|
form.with_child(
|
||||||
form::Number::new()
|
form::Number::new()
|
||||||
.with_id(util::join!(id, "-jump-page"))
|
.with_id(util::join!(id, "-jump-page"))
|
||||||
.with_prop(PropsOp::add_classes("pager-jump-input"))
|
.with_prop(PropsOp::add_classes("pager-jump-input"))
|
||||||
.with_prop(PropsOp::add_style("--pager-jump-width", jump_width))
|
|
||||||
.with_name("page")
|
.with_name("page")
|
||||||
.with_min(Some(1))
|
.with_min(Some(1))
|
||||||
.with_max(Some(total_pages))
|
.with_max(Some(total_pages))
|
||||||
.with_value(Some(page)),
|
.with_value(Some(page))
|
||||||
|
.with_label(L10n::l("pager_goto_label")),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
Button::submit(L10n::l("pager_goto_button"))
|
Button::submit(L10n::l("pager_goto_button"))
|
||||||
.with_prop(PropsOp::add_classes("pager-jump-button"))
|
.with_prop(PropsOp::add_classes("pager-jump-button")),
|
||||||
.with_title(L10n::l("pager_goto_label")),
|
|
||||||
)
|
)
|
||||||
.render(cx).await
|
.render(cx).await
|
||||||
}) }
|
}) }
|
||||||
|
|
@ -397,27 +332,6 @@ impl Pager {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece la alineación horizontal del paginador dentro de su contenedor. Por defecto es
|
|
||||||
/// [`PagerAlign::Center`].
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_align(mut self, align: PagerAlign) -> Self {
|
|
||||||
self.align = align;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la visibilidad del resumen de las páginas que se muestran en cada vista. Por
|
|
||||||
/// defecto es `PagerVisibility::Auto`: sólo se muestra cuando el número total de páginas supera
|
|
||||||
/// al número de páginas que se muestra en el paginador, igual que [`with_prev_next()`] y
|
|
||||||
/// [`with_jump()`].
|
|
||||||
///
|
|
||||||
/// [`with_prev_next()`]: Self::with_prev_next
|
|
||||||
/// [`with_jump()`]: Self::with_jump
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_summary(mut self, summary: PagerVisibility) -> Self {
|
|
||||||
self.summary = summary;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Establece la visibilidad de los botones de página anterior/siguiente. Por defecto es
|
/// Establece la visibilidad de los botones de página anterior/siguiente. Por defecto es
|
||||||
/// `PagerVisibility::Auto`: sólo se muestran cuando el número total de páginas supera al
|
/// `PagerVisibility::Auto`: sólo se muestran cuando el número total de páginas supera al
|
||||||
/// número de páginas que se muestra en el paginador (con el extremo correspondiente
|
/// número de páginas que se muestra en el paginador (con el extremo correspondiente
|
||||||
|
|
@ -475,34 +389,8 @@ impl Pager {
|
||||||
return (1..=total_pages).map(PageItem::Number).collect();
|
return (1..=total_pages).map(PageItem::Number).collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ventana centrada en `page`, protegiendo las operaciones aritméticas con signo.
|
let low = page.saturating_sub(window).max(2);
|
||||||
let mut low = page as i128 - window as i128;
|
let high = page.saturating_add(window).min(total_pages - 1);
|
||||||
let mut high = page as i128 + window as i128;
|
|
||||||
|
|
||||||
if low < 2 {
|
|
||||||
let overflow = 2 - low;
|
|
||||||
low += overflow;
|
|
||||||
high += overflow;
|
|
||||||
}
|
|
||||||
if high > total_pages as i128 - 1 {
|
|
||||||
let overflow = high - (total_pages as i128 - 1);
|
|
||||||
high -= overflow;
|
|
||||||
low -= overflow;
|
|
||||||
}
|
|
||||||
low = low.clamp(2, total_pages as i128 - 1);
|
|
||||||
high = high.clamp(2, total_pages as i128 - 1);
|
|
||||||
|
|
||||||
// Si la ventana toca la primera o la última página, ese lado no necesita elipsis ni número
|
|
||||||
// de relleno: el hueco que se ahorra se reinvierte ampliando la ventana por el otro lado.
|
|
||||||
if low == 2 {
|
|
||||||
high = (high + 1).min(total_pages as i128 - 1);
|
|
||||||
}
|
|
||||||
if high == total_pages as i128 - 1 {
|
|
||||||
low = (low - 1).max(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
let low = low as u64;
|
|
||||||
let high = high as u64;
|
|
||||||
|
|
||||||
let mut items = vec![PageItem::Number(1)];
|
let mut items = vec![PageItem::Number(1)];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,7 @@ poweredby_pagetop = Powered by { $pagetop_link }
|
||||||
|
|
||||||
# Pager component.
|
# Pager component.
|
||||||
pager_aria_label = Page navigation
|
pager_aria_label = Page navigation
|
||||||
pager_previous_label = Previous
|
pager_previous_label = Previous page
|
||||||
pager_previous_aria_label = Previous page
|
pager_next_label = Next page
|
||||||
pager_next_label = Next
|
pager_goto_label = Go to page
|
||||||
pager_next_aria_label = Next page
|
|
||||||
pager_goto_label = Jump to page
|
|
||||||
pager_goto_button = Go
|
pager_goto_button = Go
|
||||||
pager_summary = Showing { $first }-{ $last } of { $total }
|
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,7 @@ poweredby_pagetop = Funciona con { $pagetop_link }
|
||||||
|
|
||||||
# Pager component.
|
# Pager component.
|
||||||
pager_aria_label = Navegación de páginas
|
pager_aria_label = Navegación de páginas
|
||||||
pager_previous_label = Anterior
|
pager_previous_label = Página anterior
|
||||||
pager_previous_aria_label = Página anterior
|
pager_next_label = Página siguiente
|
||||||
pager_next_label = Siguiente
|
pager_goto_label = Ir a la página
|
||||||
pager_next_aria_label = Página siguiente
|
|
||||||
pager_goto_label = Saltar a la página
|
|
||||||
pager_goto_button = Ir
|
pager_goto_button = Ir
|
||||||
pager_summary = Mostrando { $first }-{ $last } de { $total }
|
|
||||||
|
|
|
||||||
|
|
@ -40,90 +40,6 @@ async fn with_aria_label_overrides_the_default() {
|
||||||
assert!(!html.contains(r#"aria-label="Page navigation""#));
|
assert!(!html.contains(r#"aria-label="Page navigation""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn summary_is_hidden_by_default_when_not_truncated() {
|
|
||||||
// 97 items at 20 per page is only 5 pages: with the default window (2) that fits without
|
|
||||||
// truncation, so `PagerVisibility::Auto` keeps the summary hidden.
|
|
||||||
let mut pager = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(1)
|
|
||||||
.with_items_per_page(20)
|
|
||||||
.with_total_items(97);
|
|
||||||
|
|
||||||
let html = pager.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert!(!html.contains("pager-summary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn summary_is_shown_by_default_when_truncated() {
|
|
||||||
let mut pager = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(10)
|
|
||||||
.with_items_per_page(1)
|
|
||||||
.with_total_items(20);
|
|
||||||
|
|
||||||
let html = pager.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains("pager-summary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn summary_shows_the_range_of_the_current_page() {
|
|
||||||
let mut first_page = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(1)
|
|
||||||
.with_items_per_page(20)
|
|
||||||
.with_total_items(97)
|
|
||||||
.with_summary(PagerVisibility::Always);
|
|
||||||
let html = first_page
|
|
||||||
.render(&mut Context::default())
|
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
assert!(html.contains(r#"<span class="pager-summary">Showing 1-20 of 97</span>"#));
|
|
||||||
|
|
||||||
// Last page: fewer items than `items_per_page`, so `last` stops at `total_items`.
|
|
||||||
let mut last_page = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(5)
|
|
||||||
.with_items_per_page(20)
|
|
||||||
.with_total_items(97)
|
|
||||||
.with_summary(PagerVisibility::Always);
|
|
||||||
let html = last_page
|
|
||||||
.render(&mut Context::default())
|
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
assert!(html.contains(r#"<span class="pager-summary">Showing 81-97 of 97</span>"#));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn summary_can_be_forced_to_always_show_even_when_not_truncated() {
|
|
||||||
let mut pager = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(1)
|
|
||||||
.with_items_per_page(20)
|
|
||||||
.with_total_items(97)
|
|
||||||
.with_summary(PagerVisibility::Always);
|
|
||||||
|
|
||||||
let html = pager.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert!(html.contains("pager-summary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn summary_never_shows_it_even_when_truncated() {
|
|
||||||
let mut pager = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(10)
|
|
||||||
.with_items_per_page(1)
|
|
||||||
.with_total_items(20)
|
|
||||||
.with_summary(PagerVisibility::Never);
|
|
||||||
|
|
||||||
let html = pager.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert!(!html.contains("pager-summary"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn renders_page_links_and_current_page() {
|
async fn renders_page_links_and_current_page() {
|
||||||
let mut pager = Pager::new()
|
let mut pager = Pager::new()
|
||||||
|
|
@ -397,128 +313,6 @@ async fn current_page_near_an_edge_does_not_panic_and_keeps_first_and_last() {
|
||||||
assert!(html.contains(r#"href="/list?page=20""#));
|
assert!(html.contains(r#"href="/list?page=20""#));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn window_max_cell_count_follows_2_window_plus_5() {
|
|
||||||
// Max cells = first + last + one filler slot each side (number or ellipsis) + the numbers in
|
|
||||||
// the window itself (2 * window + 1) = 2 * window + 5. Must hold both for a centered page
|
|
||||||
// (baseline case) and near an edge (padded case), for any window size.
|
|
||||||
for window in [1_u64, 2, 3, 4, 5] {
|
|
||||||
let max_cells = 2 * window + 5;
|
|
||||||
|
|
||||||
let mut centered = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(500)
|
|
||||||
.with_items_per_page(1)
|
|
||||||
.with_total_items(1000)
|
|
||||||
.with_window(window)
|
|
||||||
.with_prev_next(PagerVisibility::Never);
|
|
||||||
let html = centered.render(&mut Context::default()).await.into_string();
|
|
||||||
assert_eq!(
|
|
||||||
html.matches("page-item").count() as u64,
|
|
||||||
max_cells,
|
|
||||||
"window={window}, centered page"
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut near_edge = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(1)
|
|
||||||
.with_items_per_page(1)
|
|
||||||
.with_total_items(1000)
|
|
||||||
.with_window(window)
|
|
||||||
.with_prev_next(PagerVisibility::Never);
|
|
||||||
let html = near_edge
|
|
||||||
.render(&mut Context::default())
|
|
||||||
.await
|
|
||||||
.into_string();
|
|
||||||
assert_eq!(
|
|
||||||
html.matches("page-item").count() as u64,
|
|
||||||
max_cells,
|
|
||||||
"window={window}, page 1"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn window_pads_out_as_documented_in_the_module_example() {
|
|
||||||
// Matches the exact example in the doc comment of `Pager`.
|
|
||||||
let mut pager = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(1)
|
|
||||||
.with_items_per_page(1)
|
|
||||||
.with_total_items(200)
|
|
||||||
.with_window(3);
|
|
||||||
|
|
||||||
let html = pager.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert_eq!(html.matches("page-ellipsis").count(), 1);
|
|
||||||
for page in [1, 2, 3, 4, 5, 6, 7, 8, 9, 200] {
|
|
||||||
assert!(
|
|
||||||
html.contains(&format!(r#"href="/list?page={page}""#)),
|
|
||||||
"expected page {page} to be visible"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for page in [10, 11, 199] {
|
|
||||||
assert!(
|
|
||||||
!html.contains(&format!(r#"href="/list?page={page}""#)),
|
|
||||||
"expected page {page} to be hidden behind an ellipsis"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn window_pads_out_to_a_constant_cell_count_near_the_first_page() {
|
|
||||||
let mut pager = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(1)
|
|
||||||
.with_items_per_page(1)
|
|
||||||
.with_total_items(188)
|
|
||||||
.with_window(2);
|
|
||||||
|
|
||||||
let html = pager.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
// Near page 1 the low side of the window needs neither ellipsis nor filler number, so that gap
|
|
||||||
// is reinvested on the high side instead of just shrinking the window.
|
|
||||||
assert_eq!(html.matches("page-ellipsis").count(), 1);
|
|
||||||
for page in [1, 2, 3, 4, 5, 6, 7, 188] {
|
|
||||||
assert!(
|
|
||||||
html.contains(&format!(r#"href="/list?page={page}""#)),
|
|
||||||
"expected page {page} to be visible"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for page in [8, 9, 100, 187] {
|
|
||||||
assert!(
|
|
||||||
!html.contains(&format!(r#"href="/list?page={page}""#)),
|
|
||||||
"expected page {page} to be hidden behind an ellipsis"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
|
||||||
async fn window_pads_out_to_a_constant_cell_count_near_the_last_page() {
|
|
||||||
let mut pager = Pager::new()
|
|
||||||
.with_base_path("/list")
|
|
||||||
.with_current_page(188)
|
|
||||||
.with_items_per_page(1)
|
|
||||||
.with_total_items(188)
|
|
||||||
.with_window(2);
|
|
||||||
|
|
||||||
let html = pager.render(&mut Context::default()).await.into_string();
|
|
||||||
|
|
||||||
assert_eq!(html.matches("page-ellipsis").count(), 1);
|
|
||||||
for page in [1, 182, 183, 184, 185, 186, 187, 188] {
|
|
||||||
assert!(
|
|
||||||
html.contains(&format!(r#"href="/list?page={page}""#)),
|
|
||||||
"expected page {page} to be visible"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for page in [2, 89, 181] {
|
|
||||||
assert!(
|
|
||||||
!html.contains(&format!(r#"href="/list?page={page}""#)),
|
|
||||||
"expected page {page} to be hidden behind an ellipsis"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pagetop::test]
|
#[pagetop::test]
|
||||||
async fn small_total_is_never_truncated_even_with_a_window() {
|
async fn small_total_is_never_truncated_even_with_a_window() {
|
||||||
let mut pager = Pager::new()
|
let mut pager = Pager::new()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue