WIP: Añade componente para la gestión de menús #8
12 changed files with 602 additions and 226 deletions
|
|
@ -16,6 +16,11 @@ pub mod image;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use image::Image;
|
pub use image::Image;
|
||||||
|
|
||||||
|
// Nav.
|
||||||
|
pub mod nav;
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use nav::Nav;
|
||||||
|
|
||||||
// Navbar.
|
// Navbar.
|
||||||
pub mod navbar;
|
pub mod navbar;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
//! Definiciones para crear menús desplegables [`Dropdown`].
|
//! Definiciones para crear menús desplegables [`Dropdown`].
|
||||||
//!
|
//!
|
||||||
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
|
//! Cada [`dropdown::Item`](crate::theme::dropdown::Item) representa un elemento individual del
|
||||||
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad: enlaces de
|
//! desplegable [`Dropdown`], con distintos comportamientos según su finalidad, como enlaces de
|
||||||
//! navegación, botones de acción, encabezados o divisores visuales.
|
//! navegación, botones de acción, encabezados o divisores visuales.
|
||||||
//!
|
//!
|
||||||
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
||||||
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
||||||
//!
|
|
||||||
//! Su propósito es ofrecer una base uniforme sobre la que construir menús consistentes, adaptados
|
|
||||||
//! al contexto de cada aplicación.
|
|
||||||
|
|
||||||
mod props;
|
mod props;
|
||||||
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
|
pub use props::{AutoClose, Direction, MenuAlign, MenuPosition};
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,12 @@ use crate::LOCALES_BOOTSIER;
|
||||||
/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de
|
/// interacción del usuario. Admite variaciones de tamaño/color del botón, también dirección de
|
||||||
/// apertura, alineación o política de cierre.
|
/// apertura, alineación o política de cierre.
|
||||||
///
|
///
|
||||||
/// Sin título para el botón (ver [`with_button_title()`](Self::with_button_title)) se muestra
|
/// Si no tiene título (ver [`with_title()`](Self::with_title)) se muestra únicamente la lista de
|
||||||
/// únicamente la lista de elementos sin ningún botón para interactuar.
|
/// elementos sin ningún botón para interactuar.
|
||||||
|
///
|
||||||
|
/// Si este componente se usa en un menú [`Nav`] (ver [`nav::Item::dropdown()`]) sólo se tendrán en
|
||||||
|
/// cuenta **el título** (si no existe le asigna uno por defecto) y **la lista de elementos**; el
|
||||||
|
/// resto de propiedades no afectarán a su representación en [`Nav`].
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -19,7 +23,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
/// # use pagetop::prelude::*;
|
/// # use pagetop::prelude::*;
|
||||||
/// # use pagetop_bootsier::prelude::*;
|
/// # use pagetop_bootsier::prelude::*;
|
||||||
/// let dd = Dropdown::new()
|
/// let dd = Dropdown::new()
|
||||||
/// .with_button_title(L10n::n("Menu"))
|
/// .with_title(L10n::n("Menu"))
|
||||||
/// .with_button_color(ButtonColor::Background(Color::Secondary))
|
/// .with_button_color(ButtonColor::Background(Color::Secondary))
|
||||||
/// .with_auto_close(dropdown::AutoClose::ClickableInside)
|
/// .with_auto_close(dropdown::AutoClose::ClickableInside)
|
||||||
/// .with_direction(dropdown::Direction::Dropend)
|
/// .with_direction(dropdown::Direction::Dropend)
|
||||||
|
|
@ -34,7 +38,7 @@ use crate::LOCALES_BOOTSIER;
|
||||||
pub struct Dropdown {
|
pub struct Dropdown {
|
||||||
id : AttrId,
|
id : AttrId,
|
||||||
classes : AttrClasses,
|
classes : AttrClasses,
|
||||||
button_title : L10n,
|
title : L10n,
|
||||||
button_size : ButtonSize,
|
button_size : ButtonSize,
|
||||||
button_color : ButtonColor,
|
button_color : ButtonColor,
|
||||||
button_split : bool,
|
button_split : bool,
|
||||||
|
|
@ -80,11 +84,11 @@ impl Component for Dropdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Título opcional para el menú desplegable.
|
// Título opcional para el menú desplegable.
|
||||||
let button_title = self.button_title().using(cx);
|
let title = self.title().using(cx);
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
PrepareMarkup::With(html! {
|
||||||
div id=[self.id()] class=[self.classes().get()] {
|
div id=[self.id()] class=[self.classes().get()] {
|
||||||
@if !button_title.is_empty() {
|
@if !title.is_empty() {
|
||||||
@let mut btn_classes = AttrClasses::new([
|
@let mut btn_classes = AttrClasses::new([
|
||||||
"btn",
|
"btn",
|
||||||
&self.button_size().to_string(),
|
&self.button_size().to_string(),
|
||||||
|
|
@ -129,7 +133,7 @@ impl Component for Dropdown {
|
||||||
type="button"
|
type="button"
|
||||||
class=[btn_classes.get()]
|
class=[btn_classes.get()]
|
||||||
{
|
{
|
||||||
(button_title)
|
(title)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Botón *toggle* que abre/cierra el menú asociado.
|
// Botón *toggle* que abre/cierra el menú asociado.
|
||||||
|
|
@ -176,7 +180,7 @@ impl Component for Dropdown {
|
||||||
data-bs-auto-close=[auto_close]
|
data-bs-auto-close=[auto_close]
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
{
|
{
|
||||||
(button_title)
|
(title)
|
||||||
}
|
}
|
||||||
ul class=[menu_classes.get()] { (items) }
|
ul class=[menu_classes.get()] { (items) }
|
||||||
}
|
}
|
||||||
|
|
@ -206,10 +210,10 @@ impl Dropdown {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Establece el título del botón.
|
/// Establece el título del menú desplegable.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_button_title(mut self, title: L10n) -> Self {
|
pub fn with_title(mut self, title: L10n) -> Self {
|
||||||
self.button_title = title;
|
self.title = title;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,9 +294,9 @@ impl Dropdown {
|
||||||
&self.classes
|
&self.classes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el título del botón.
|
/// Devuelve el título del menú desplegable.
|
||||||
pub fn button_title(&self) -> &L10n {
|
pub fn title(&self) -> &L10n {
|
||||||
&self.button_title
|
&self.title
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el tamaño configurado del botón.
|
/// Devuelve el tamaño configurado del botón.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ pub enum ItemKind {
|
||||||
/// [`ItemKind`].
|
/// [`ItemKind`].
|
||||||
///
|
///
|
||||||
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
|
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
|
||||||
/// manteniendo una interfaz común para renderizar todos los ítems del menú.
|
/// manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[derive(AutoDefault)]
|
#[derive(AutoDefault)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
|
|
@ -79,7 +79,7 @@ impl Component for Item {
|
||||||
} => {
|
} => {
|
||||||
let path = path(cx);
|
let path = path(cx);
|
||||||
let current_path = cx.request().map(|request| request.path());
|
let current_path = cx.request().map(|request| request.path());
|
||||||
let is_current = !*disabled && current_path.map(|p| p == path).unwrap_or(false);
|
let is_current = !*disabled && current_path.map_or(false, |p| p == path);
|
||||||
|
|
||||||
let mut classes = "dropdown-item".to_string();
|
let mut classes = "dropdown-item".to_string();
|
||||||
if is_current {
|
if is_current {
|
||||||
|
|
@ -200,7 +200,7 @@ impl Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crea un enlace deshabilitado que se abriría en una nueva ventana.
|
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
||||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||||
Item {
|
Item {
|
||||||
item_kind: ItemKind::Link {
|
item_kind: ItemKind::Link {
|
||||||
|
|
@ -274,7 +274,7 @@ impl Item {
|
||||||
&self.classes
|
&self.classes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Devuelve el tipo de elemento representado por este ítem.
|
/// Devuelve el tipo de elemento representado por este elemento.
|
||||||
pub fn item_kind(&self) -> &ItemKind {
|
pub fn item_kind(&self) -> &ItemKind {
|
||||||
&self.item_kind
|
&self.item_kind
|
||||||
}
|
}
|
||||||
|
|
|
||||||
17
extensions/pagetop-bootsier/src/theme/nav.rs
Normal file
17
extensions/pagetop-bootsier/src/theme/nav.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
//! Definiciones para crear menús [`Nav`] o alguna de sus variantes de presentación.
|
||||||
|
//!
|
||||||
|
//! Cada [`nav::Item`](crate::theme::nav::Item) representa un elemento individual del menú [`Nav`],
|
||||||
|
//! con distintos comportamientos según su finalidad, como enlaces de navegación o menús
|
||||||
|
//! desplegables [`Dropdown`](crate::theme::Dropdown).
|
||||||
|
//!
|
||||||
|
//! Los ítems pueden estar activos, deshabilitados o abrirse en nueva ventana según su contexto y
|
||||||
|
//! configuración, y permiten incluir etiquetas localizables usando [`L10n`](pagetop::locale::L10n).
|
||||||
|
|
||||||
|
mod props;
|
||||||
|
pub use props::{Kind, Layout};
|
||||||
|
|
||||||
|
mod component;
|
||||||
|
pub use component::Nav;
|
||||||
|
|
||||||
|
mod item;
|
||||||
|
pub use item::{Item, ItemKind};
|
||||||
168
extensions/pagetop-bootsier/src/theme/nav/component.rs
Normal file
168
extensions/pagetop-bootsier/src/theme/nav/component.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Componente para crear un **menú** o alguna de sus variantes ([`nav::Kind`]).
|
||||||
|
///
|
||||||
|
/// Presenta un menú con una lista de elementos usando una vista básica, o alguna de sus variantes
|
||||||
|
/// como *pestañas* (`Tabs`), *botones* (`Pills`) o *subrayado* (`Underline`). También permite
|
||||||
|
/// controlar su distribución y orientación ([`nav::Layout`](crate::theme::nav::Layout)).
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// # use pagetop_bootsier::prelude::*;
|
||||||
|
/// let nav = Nav::tabs()
|
||||||
|
/// .with_layout(nav::Layout::End)
|
||||||
|
/// .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||||
|
/// .add_item(nav::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
|
||||||
|
/// .add_item(nav::Item::dropdown(
|
||||||
|
/// Dropdown::new()
|
||||||
|
/// .with_title(L10n::n("Options"))
|
||||||
|
/// .with_items(TypedOp::AddMany(vec![
|
||||||
|
/// Typed::with(dropdown::Item::link(L10n::n("Action"), |_| "/action")),
|
||||||
|
/// Typed::with(dropdown::Item::link(L10n::n("Another action"), |_| "/another")),
|
||||||
|
/// ])),
|
||||||
|
/// ))
|
||||||
|
/// .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"));
|
||||||
|
/// ```
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Nav {
|
||||||
|
id : AttrId,
|
||||||
|
classes : AttrClasses,
|
||||||
|
items : Children,
|
||||||
|
nav_kind : nav::Kind,
|
||||||
|
nav_layout: nav::Layout,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Nav {
|
||||||
|
fn new() -> Self {
|
||||||
|
Nav::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(
|
||||||
|
ClassesOp::Prepend,
|
||||||
|
[
|
||||||
|
"nav",
|
||||||
|
match self.nav_kind() {
|
||||||
|
nav::Kind::Default => "",
|
||||||
|
nav::Kind::Tabs => "nav-tabs",
|
||||||
|
nav::Kind::Pills => "nav-pills",
|
||||||
|
nav::Kind::Underline => "nav-underline",
|
||||||
|
},
|
||||||
|
match self.nav_layout() {
|
||||||
|
nav::Layout::Default => "",
|
||||||
|
nav::Layout::Start => "justify-content-start",
|
||||||
|
nav::Layout::Center => "justify-content-center",
|
||||||
|
nav::Layout::End => "justify-content-end",
|
||||||
|
nav::Layout::Vertical => "flex-column",
|
||||||
|
nav::Layout::Fill => "nav-fill",
|
||||||
|
nav::Layout::Justified => "nav-justified",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.join(" "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
let items = self.items().render(cx);
|
||||||
|
if items.is_empty() {
|
||||||
|
return PrepareMarkup::None;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
ul id=[self.id()] class=[self.classes().get()] {
|
||||||
|
(items)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Nav {
|
||||||
|
/// Crea un `Nav` usando pestañas para los elementos (*Tabs*).
|
||||||
|
pub fn tabs() -> Self {
|
||||||
|
Nav::default().with_kind(nav::Kind::Tabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un `Nav` usando botones para los elementos (*Pills*).
|
||||||
|
pub fn pills() -> Self {
|
||||||
|
Nav::default().with_kind(nav::Kind::Pills)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un `Nav` usando elementos subrayados (*Underline*).
|
||||||
|
pub fn underline() -> Self {
|
||||||
|
Nav::default().with_kind(nav::Kind::Underline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Nav BUILDER >****************************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único (`id`) del menú.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de clases CSS aplicadas al menú.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cambia el estilo del menú (*Tabs*, *Pills*, *Underline* o *Default*).
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_kind(mut self, kind: nav::Kind) -> Self {
|
||||||
|
self.nav_kind = kind;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selecciona la distribución y orientación del menú.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_layout(mut self, layout: nav::Layout) -> Self {
|
||||||
|
self.nav_layout = layout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Añade un nuevo elemento hijo al menú.
|
||||||
|
pub fn add_item(mut self, item: nav::Item) -> Self {
|
||||||
|
self.items.add(Child::with(item));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_items(mut self, op: TypedOp<nav::Item>) -> Self {
|
||||||
|
self.items.alter_typed(op);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Nav GETTERS >****************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve las clases CSS asociadas al menú.
|
||||||
|
pub fn classes(&self) -> &AttrClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el estilo visual seleccionado.
|
||||||
|
pub fn nav_kind(&self) -> &nav::Kind {
|
||||||
|
&self.nav_kind
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve la distribución y orientación seleccionada.
|
||||||
|
pub fn nav_layout(&self) -> &nav::Layout {
|
||||||
|
&self.nav_layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve la lista de elementos (`children`) del menú.
|
||||||
|
pub fn items(&self) -> &Children {
|
||||||
|
&self.items
|
||||||
|
}
|
||||||
|
}
|
||||||
257
extensions/pagetop-bootsier/src/theme/nav/item.rs
Normal file
257
extensions/pagetop-bootsier/src/theme/nav/item.rs
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::LOCALES_BOOTSIER;
|
||||||
|
|
||||||
|
// **< ItemKind >***********************************************************************************
|
||||||
|
|
||||||
|
/// Tipos de [`nav::Item`](crate::theme::nav::Item) disponibles en un menú
|
||||||
|
/// [`Nav`](crate::theme::Nav).
|
||||||
|
///
|
||||||
|
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||||
|
/// con él.
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum ItemKind {
|
||||||
|
/// Elemento vacío, no produce salida.
|
||||||
|
#[default]
|
||||||
|
Void,
|
||||||
|
/// Etiqueta sin comportamiento interactivo.
|
||||||
|
Label(L10n),
|
||||||
|
/// Elemento de navegación. Opcionalmente puede abrirse en una nueva ventana y estar
|
||||||
|
/// inicialmente deshabilitado.
|
||||||
|
Link {
|
||||||
|
label: L10n,
|
||||||
|
path: FnPathByContext,
|
||||||
|
blank: bool,
|
||||||
|
disabled: bool,
|
||||||
|
},
|
||||||
|
/// Elemento que despliega un menú [`Dropdown`].
|
||||||
|
Dropdown(Typed<Dropdown>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Item >***************************************************************************************
|
||||||
|
|
||||||
|
/// Representa un **elemento individual** de un menú [`Nav`](crate::theme::Nav).
|
||||||
|
///
|
||||||
|
/// Cada instancia de [`nav::Item`](crate::theme::nav::Item) se traduce en un componente visible que
|
||||||
|
/// puede comportarse como texto, enlace, botón o menú desplegable según su [`ItemKind`].
|
||||||
|
///
|
||||||
|
/// Permite definir identificador, clases de estilo adicionales o tipo de interacción asociada,
|
||||||
|
/// manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub struct Item {
|
||||||
|
id : AttrId,
|
||||||
|
classes : AttrClasses,
|
||||||
|
item_kind: ItemKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Item {
|
||||||
|
fn new() -> Self {
|
||||||
|
Item::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> Option<String> {
|
||||||
|
self.id.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||||
|
self.alter_classes(
|
||||||
|
ClassesOp::Prepend,
|
||||||
|
if matches!(self.item_kind(), ItemKind::Dropdown(_)) {
|
||||||
|
"nav-item dropdown"
|
||||||
|
} else {
|
||||||
|
"nav-item"
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||||
|
match self.item_kind() {
|
||||||
|
ItemKind::Void => PrepareMarkup::None,
|
||||||
|
|
||||||
|
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||||
|
li id=[self.id()] class=[self.classes().get()] {
|
||||||
|
span {
|
||||||
|
(label.using(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
ItemKind::Link {
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
blank,
|
||||||
|
disabled,
|
||||||
|
} => {
|
||||||
|
let path = path(cx);
|
||||||
|
let current_path = cx.request().map(|request| request.path());
|
||||||
|
let is_current = !*disabled && current_path.map_or(false, |p| p == path);
|
||||||
|
|
||||||
|
let mut classes = "nav-link".to_string();
|
||||||
|
if is_current {
|
||||||
|
classes.push_str(" active");
|
||||||
|
}
|
||||||
|
if *disabled {
|
||||||
|
classes.push_str(" disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
let href = (!disabled).then_some(path);
|
||||||
|
let target = (!disabled && *blank).then_some("_blank");
|
||||||
|
let rel = (!disabled && *blank).then_some("noopener noreferrer");
|
||||||
|
|
||||||
|
let aria_current = (href.is_some() && is_current).then_some("page");
|
||||||
|
let aria_disabled = disabled.then_some("true");
|
||||||
|
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
li id=[self.id()] class=[self.classes().get()] {
|
||||||
|
a
|
||||||
|
class=(classes)
|
||||||
|
href=[href]
|
||||||
|
target=[target]
|
||||||
|
rel=[rel]
|
||||||
|
aria-current=[aria_current]
|
||||||
|
aria-disabled=[aria_disabled]
|
||||||
|
{
|
||||||
|
(label.using(cx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemKind::Dropdown(menu) => {
|
||||||
|
if let Some(dd) = menu.borrow() {
|
||||||
|
let items = dd.items().render(cx);
|
||||||
|
if items.is_empty() {
|
||||||
|
return PrepareMarkup::None;
|
||||||
|
}
|
||||||
|
let title = dd.title().lookup(cx).unwrap_or_else(|| {
|
||||||
|
L10n::t("dropdown", &LOCALES_BOOTSIER)
|
||||||
|
.lookup(cx)
|
||||||
|
.unwrap_or_else(|| "Dropdown".to_string())
|
||||||
|
});
|
||||||
|
PrepareMarkup::With(html! {
|
||||||
|
li id=[self.id()] class=[self.classes().get()] {
|
||||||
|
a
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
href="#"
|
||||||
|
role="button"
|
||||||
|
aria-expanded="false"
|
||||||
|
{
|
||||||
|
(title)
|
||||||
|
}
|
||||||
|
ul class="dropdown-menu" {
|
||||||
|
(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
PrepareMarkup::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item {
|
||||||
|
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||||
|
pub fn label(label: L10n) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Label(label),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un enlace para la navegación.
|
||||||
|
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Link {
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
blank: false,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||||
|
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Link {
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
blank: false,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||||
|
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Link {
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
blank: true,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
||||||
|
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Link {
|
||||||
|
label,
|
||||||
|
path,
|
||||||
|
blank: true,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crea un elemento de navegación que contiene un menú desplegable [`Dropdown`].
|
||||||
|
///
|
||||||
|
/// Sólo se tienen en cuenta **el título** (si no existe le asigna uno por defecto) y **la lista
|
||||||
|
/// de elementos** del [`Dropdown`]; el resto de propiedades del componente no afectarán a su
|
||||||
|
/// representación en [`Nav`].
|
||||||
|
pub fn dropdown(menu: Dropdown) -> Self {
|
||||||
|
Item {
|
||||||
|
item_kind: ItemKind::Dropdown(Typed::with(menu)),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Item BUILDER >***************************************************************************
|
||||||
|
|
||||||
|
/// Establece el identificador único (`id`) del elemento.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||||
|
self.id.alter_value(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Modifica la lista de clases CSS aplicadas al elemento.
|
||||||
|
#[builder_fn]
|
||||||
|
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||||
|
self.classes.alter_value(op, classes);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Item GETTERS >***************************************************************************
|
||||||
|
|
||||||
|
/// Devuelve las clases CSS asociadas al elemento.
|
||||||
|
pub fn classes(&self) -> &AttrClasses {
|
||||||
|
&self.classes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve el tipo de elemento representado por este elemento.
|
||||||
|
pub fn item_kind(&self) -> &ItemKind {
|
||||||
|
&self.item_kind
|
||||||
|
}
|
||||||
|
}
|
||||||
39
extensions/pagetop-bootsier/src/theme/nav/props.rs
Normal file
39
extensions/pagetop-bootsier/src/theme/nav/props.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
|
// **< Kind >***************************************************************************************
|
||||||
|
|
||||||
|
/// Define la variante de presentación de un menú [`Nav`](crate::theme::Nav).
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum Kind {
|
||||||
|
/// Estilo por defecto, lista de enlaces flexible y minimalista.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Pestañas con borde para cambiar entre secciones.
|
||||||
|
Tabs,
|
||||||
|
/// Botones con fondo que resaltan el elemento activo.
|
||||||
|
Pills,
|
||||||
|
/// Variante con subrayado del elemento activo, estética ligera.
|
||||||
|
Underline,
|
||||||
|
}
|
||||||
|
|
||||||
|
// **< Layout >*************************************************************************************
|
||||||
|
|
||||||
|
/// Distribución y orientación de un menú [`Nav`](crate::theme::Nav).
|
||||||
|
#[derive(AutoDefault)]
|
||||||
|
pub enum Layout {
|
||||||
|
/// Comportamiento por defecto, ancho definido por el contenido y sin alineación forzada.
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
/// Alinea los elementos al inicio de la fila.
|
||||||
|
Start,
|
||||||
|
/// Centra horizontalmente los elementos.
|
||||||
|
Center,
|
||||||
|
/// Alinea los elementos al final de la fila.
|
||||||
|
End,
|
||||||
|
/// Apila los elementos en columna.
|
||||||
|
Vertical,
|
||||||
|
/// Los elementos se expanden para rellenar la fila.
|
||||||
|
Fill,
|
||||||
|
/// Todos los elementos ocupan el mismo ancho rellenando la fila.
|
||||||
|
Justified,
|
||||||
|
}
|
||||||
|
|
@ -9,9 +9,3 @@ pub use content::{Content, ContentType};
|
||||||
|
|
||||||
mod brand;
|
mod brand;
|
||||||
pub use brand::Brand;
|
pub use brand::Brand;
|
||||||
|
|
||||||
mod nav;
|
|
||||||
pub use nav::Nav;
|
|
||||||
|
|
||||||
mod item;
|
|
||||||
pub use item::{Item, ItemType};
|
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::Dropdown;
|
|
||||||
|
|
||||||
type Label = L10n;
|
|
||||||
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub enum ItemType {
|
|
||||||
#[default]
|
|
||||||
Void,
|
|
||||||
Label(Label),
|
|
||||||
Link(Label, FnPathByContext),
|
|
||||||
LinkBlank(Label, FnPathByContext),
|
|
||||||
Dropdown(Typed<Dropdown>),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item.
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Item {
|
|
||||||
item_type: ItemType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Item {
|
|
||||||
fn new() -> Self {
|
|
||||||
Item::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
let description: Option<String> = None;
|
|
||||||
|
|
||||||
// Obtiene la URL actual desde `cx.request`.
|
|
||||||
let current_path = cx.request().map(|request| request.path());
|
|
||||||
|
|
||||||
match self.item_type() {
|
|
||||||
ItemType::Void => PrepareMarkup::None,
|
|
||||||
ItemType::Label(label) => PrepareMarkup::With(html! {
|
|
||||||
li class="nav-item" {
|
|
||||||
span title=[description] {
|
|
||||||
//(left_icon)
|
|
||||||
(label.using(cx))
|
|
||||||
//(right_icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ItemType::Link(label, path) => {
|
|
||||||
let item_path = path(cx);
|
|
||||||
let (class, aria) = if current_path == Some(item_path) {
|
|
||||||
("nav-item active", Some("page"))
|
|
||||||
} else {
|
|
||||||
("nav-item", None)
|
|
||||||
};
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
li class=(class) aria-current=[aria] {
|
|
||||||
a class="nav-link" href=(item_path) title=[description] {
|
|
||||||
//(left_icon)
|
|
||||||
(label.using(cx))
|
|
||||||
//(right_icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ItemType::LinkBlank(label, path) => {
|
|
||||||
let item_path = path(cx);
|
|
||||||
let (class, aria) = if current_path == Some(item_path) {
|
|
||||||
("nav-item active", Some("page"))
|
|
||||||
} else {
|
|
||||||
("nav-item", None)
|
|
||||||
};
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
li class=(class) aria-current=[aria] {
|
|
||||||
a class="nav-link" href=(item_path) title=[description] target="_blank" {
|
|
||||||
//(left_icon)
|
|
||||||
(label.using(cx))
|
|
||||||
//(right_icon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ItemType::Dropdown(menu) => PrepareMarkup::With(html! { (menu.render(cx)) }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item {
|
|
||||||
pub fn label(label: L10n) -> Self {
|
|
||||||
Item {
|
|
||||||
item_type: ItemType::Label(label),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_type: ItemType::Link(label, path),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
|
||||||
Item {
|
|
||||||
item_type: ItemType::LinkBlank(label, path),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item GETTERS.
|
|
||||||
|
|
||||||
pub fn item_type(&self) -> &ItemType {
|
|
||||||
&self.item_type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
use pagetop::prelude::*;
|
|
||||||
|
|
||||||
use crate::theme::navbar;
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[derive(AutoDefault)]
|
|
||||||
pub struct Nav {
|
|
||||||
id : AttrId,
|
|
||||||
classes: AttrClasses,
|
|
||||||
items : Children,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component for Nav {
|
|
||||||
fn new() -> Self {
|
|
||||||
Nav::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> Option<String> {
|
|
||||||
self.id.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
|
||||||
self.alter_classes(ClassesOp::Prepend, "navbar-nav");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
|
||||||
let items = self.items().render(cx);
|
|
||||||
if items.is_empty() {
|
|
||||||
return PrepareMarkup::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
PrepareMarkup::With(html! {
|
|
||||||
ul id=[self.id()] class=[self.classes().get()] {
|
|
||||||
(items)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Nav {
|
|
||||||
// Nav BUILDER.
|
|
||||||
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
|
||||||
self.id.alter_value(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
|
||||||
self.classes.alter_value(op, classes);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_item(mut self, item: navbar::Item) -> Self {
|
|
||||||
self.items.add(Child::with(item));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[builder_fn]
|
|
||||||
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
|
|
||||||
self.items.alter_typed(op);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nav GETTERS.
|
|
||||||
|
|
||||||
pub fn classes(&self) -> &AttrClasses {
|
|
||||||
&self.classes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn items(&self) -> &Children {
|
|
||||||
&self.items
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,9 @@ use crate::{builder_fn, AutoDefault, UniqueId};
|
||||||
|
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
|
pub use parking_lot::RwLockReadGuard as ComponentReadGuard;
|
||||||
|
pub use parking_lot::RwLockWriteGuard as ComponentWriteGuard;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::vec::IntoIter;
|
use std::vec::IntoIter;
|
||||||
|
|
||||||
|
|
@ -93,6 +96,57 @@ impl<C: Component> Typed<C> {
|
||||||
self.0.as_ref().and_then(|c| c.read().id())
|
self.0.as_ref().and_then(|c| c.read().id())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Devuelve una **referencia inmutable** al componente interno.
|
||||||
|
///
|
||||||
|
/// - Devuelve `Some(ComponentReadGuard<C>)` si existe el componente, o `None` si está vacío.
|
||||||
|
/// - Permite realizar **múltiples lecturas concurrentes**.
|
||||||
|
/// - Mientras el *guard* esté activo, no se pueden realizar escrituras concurrentes (ver
|
||||||
|
/// [`borrow_mut`](Self::borrow_mut)).
|
||||||
|
/// - Se recomienda mantener el *guard* **el menor tiempo posible** para evitar bloqueos
|
||||||
|
/// innecesarios.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// Lectura del nombre del componente:
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// let typed = Typed::with(Html::with(|_| html! { "Prueba" }));
|
||||||
|
/// {
|
||||||
|
/// if let Some(component) = typed.borrow() {
|
||||||
|
/// assert_eq!(component.name(), "Html");
|
||||||
|
/// }
|
||||||
|
/// }; // El *guard* se libera aquí, antes del *drop* de `typed`.
|
||||||
|
/// ```
|
||||||
|
pub fn borrow(&self) -> Option<ComponentReadGuard<'_, C>> {
|
||||||
|
self.0.as_ref().map(|a| a.read())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtiene una **referencia mutable exclusiva** al componente interno.
|
||||||
|
///
|
||||||
|
/// - Devuelve `Some(ComponentWriteGuard<C>)` si existe el componente, o `None` si está vacío.
|
||||||
|
/// - **Exclusivo**: mientras el *guard* esté activo, no habrá otros lectores ni escritores.
|
||||||
|
/// - Usar sólo para operaciones que **modifican** el estado interno.
|
||||||
|
/// - Igual que con [`borrow`](Self::borrow), se recomienda mantener el *guard* en un **ámbito
|
||||||
|
/// reducido**.
|
||||||
|
///
|
||||||
|
/// # Ejemplo
|
||||||
|
///
|
||||||
|
/// Acceso mutable (ámbito corto):
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use pagetop::prelude::*;
|
||||||
|
/// let typed = Typed::with(Block::new().with_title(L10n::n("Título")));
|
||||||
|
/// {
|
||||||
|
/// if let Some(mut component) = typed.borrow_mut() {
|
||||||
|
/// component.alter_title(L10n::n("Nuevo título"));
|
||||||
|
/// }
|
||||||
|
/// }; // El *guard* se libera aquí, antes del *drop* de `typed`.
|
||||||
|
/// ```
|
||||||
|
pub fn borrow_mut(&self) -> Option<ComponentWriteGuard<'_, C>> {
|
||||||
|
self.0.as_ref().map(|a| a.write())
|
||||||
|
}
|
||||||
|
|
||||||
// **< Typed RENDER >***************************************************************************
|
// **< Typed RENDER >***************************************************************************
|
||||||
|
|
||||||
/// Renderiza el componente con el contexto proporcionado.
|
/// Renderiza el componente con el contexto proporcionado.
|
||||||
|
|
@ -102,9 +156,9 @@ impl<C: Component> Typed<C> {
|
||||||
|
|
||||||
// **< Typed HELPERS >**************************************************************************
|
// **< Typed HELPERS >**************************************************************************
|
||||||
|
|
||||||
// Convierte el componente tipado en un [`Child`].
|
// Método interno para convertir un componente tipado en un [`Child`].
|
||||||
#[inline]
|
#[inline]
|
||||||
fn into_child(self) -> Child {
|
fn into(self) -> Child {
|
||||||
if let Some(c) = &self.0 {
|
if let Some(c) = &self.0 {
|
||||||
Child(Some(c.clone()))
|
Child(Some(c.clone()))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -115,12 +169,14 @@ impl<C: Component> Typed<C> {
|
||||||
|
|
||||||
// *************************************************************************************************
|
// *************************************************************************************************
|
||||||
|
|
||||||
/// Operaciones con un componente hijo [`Child`] en una lista [`Children`].
|
/// Operaciones para componentes hijo [`Child`] en una lista [`Children`].
|
||||||
pub enum ChildOp {
|
pub enum ChildOp {
|
||||||
Add(Child),
|
Add(Child),
|
||||||
|
AddMany(Vec<Child>),
|
||||||
InsertAfterId(&'static str, Child),
|
InsertAfterId(&'static str, Child),
|
||||||
InsertBeforeId(&'static str, Child),
|
InsertBeforeId(&'static str, Child),
|
||||||
Prepend(Child),
|
Prepend(Child),
|
||||||
|
PrependMany(Vec<Child>),
|
||||||
RemoveById(&'static str),
|
RemoveById(&'static str),
|
||||||
ReplaceById(&'static str, Child),
|
ReplaceById(&'static str, Child),
|
||||||
Reset,
|
Reset,
|
||||||
|
|
@ -129,9 +185,11 @@ pub enum ChildOp {
|
||||||
/// Operaciones con un componente hijo tipado [`Typed<C>`] en una lista [`Children`].
|
/// Operaciones con un componente hijo tipado [`Typed<C>`] en una lista [`Children`].
|
||||||
pub enum TypedOp<C: Component> {
|
pub enum TypedOp<C: Component> {
|
||||||
Add(Typed<C>),
|
Add(Typed<C>),
|
||||||
|
AddMany(Vec<Typed<C>>),
|
||||||
InsertAfterId(&'static str, Typed<C>),
|
InsertAfterId(&'static str, Typed<C>),
|
||||||
InsertBeforeId(&'static str, Typed<C>),
|
InsertBeforeId(&'static str, Typed<C>),
|
||||||
Prepend(Typed<C>),
|
Prepend(Typed<C>),
|
||||||
|
PrependMany(Vec<Typed<C>>),
|
||||||
RemoveById(&'static str),
|
RemoveById(&'static str),
|
||||||
ReplaceById(&'static str, Typed<C>),
|
ReplaceById(&'static str, Typed<C>),
|
||||||
Reset,
|
Reset,
|
||||||
|
|
@ -172,9 +230,11 @@ impl Children {
|
||||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
pub fn with_child(mut self, op: ChildOp) -> Self {
|
||||||
match op {
|
match op {
|
||||||
ChildOp::Add(any) => self.add(any),
|
ChildOp::Add(any) => self.add(any),
|
||||||
|
ChildOp::AddMany(many) => self.add_many(many),
|
||||||
ChildOp::InsertAfterId(id, any) => self.insert_after_id(id, any),
|
ChildOp::InsertAfterId(id, any) => self.insert_after_id(id, any),
|
||||||
ChildOp::InsertBeforeId(id, any) => self.insert_before_id(id, any),
|
ChildOp::InsertBeforeId(id, any) => self.insert_before_id(id, any),
|
||||||
ChildOp::Prepend(any) => self.prepend(any),
|
ChildOp::Prepend(any) => self.prepend(any),
|
||||||
|
ChildOp::PrependMany(many) => self.prepend_many(many),
|
||||||
ChildOp::RemoveById(id) => self.remove_by_id(id),
|
ChildOp::RemoveById(id) => self.remove_by_id(id),
|
||||||
ChildOp::ReplaceById(id, any) => self.replace_by_id(id, any),
|
ChildOp::ReplaceById(id, any) => self.replace_by_id(id, any),
|
||||||
ChildOp::Reset => self.reset(),
|
ChildOp::Reset => self.reset(),
|
||||||
|
|
@ -183,14 +243,16 @@ impl Children {
|
||||||
|
|
||||||
/// Ejecuta una operación con [`TypedOp`] en la lista.
|
/// Ejecuta una operación con [`TypedOp`] en la lista.
|
||||||
#[builder_fn]
|
#[builder_fn]
|
||||||
pub fn with_typed<C: Component + Default>(mut self, op: TypedOp<C>) -> Self {
|
pub fn with_typed<C: Component>(mut self, op: TypedOp<C>) -> Self {
|
||||||
match op {
|
match op {
|
||||||
TypedOp::Add(typed) => self.add(typed.into_child()),
|
TypedOp::Add(typed) => self.add(typed.into()),
|
||||||
TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.into_child()),
|
TypedOp::AddMany(many) => self.add_many(many.into_iter().map(Typed::<C>::into)),
|
||||||
TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.into_child()),
|
TypedOp::InsertAfterId(id, typed) => self.insert_after_id(id, typed.into()),
|
||||||
TypedOp::Prepend(typed) => self.prepend(typed.into_child()),
|
TypedOp::InsertBeforeId(id, typed) => self.insert_before_id(id, typed.into()),
|
||||||
|
TypedOp::Prepend(typed) => self.prepend(typed.into()),
|
||||||
|
TypedOp::PrependMany(many) => self.prepend_many(many.into_iter().map(Typed::<C>::into)),
|
||||||
TypedOp::RemoveById(id) => self.remove_by_id(id),
|
TypedOp::RemoveById(id) => self.remove_by_id(id),
|
||||||
TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.into_child()),
|
TypedOp::ReplaceById(id, typed) => self.replace_by_id(id, typed.into()),
|
||||||
TypedOp::Reset => self.reset(),
|
TypedOp::Reset => self.reset(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,7 +292,7 @@ impl Children {
|
||||||
/// Devuelve un iterador sobre los componentes hijo con el identificador de tipo ([`UniqueId`])
|
/// Devuelve un iterador sobre los componentes hijo con el identificador de tipo ([`UniqueId`])
|
||||||
/// indicado.
|
/// indicado.
|
||||||
pub fn iter_by_type_id(&self, type_id: UniqueId) -> impl Iterator<Item = &Child> {
|
pub fn iter_by_type_id(&self, type_id: UniqueId) -> impl Iterator<Item = &Child> {
|
||||||
self.0.iter().filter(move |&c| c.type_id() == Some(type_id))
|
self.0.iter().filter(move |c| c.type_id() == Some(type_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< Children RENDER >************************************************************************
|
// **< Children RENDER >************************************************************************
|
||||||
|
|
@ -246,6 +308,16 @@ impl Children {
|
||||||
|
|
||||||
// **< Children HELPERS >***********************************************************************
|
// **< Children HELPERS >***********************************************************************
|
||||||
|
|
||||||
|
// Añade más de un componente hijo al final de la lista (en el orden recibido).
|
||||||
|
#[inline]
|
||||||
|
fn add_many<I>(&mut self, iter: I) -> &mut Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Child>,
|
||||||
|
{
|
||||||
|
self.0.extend(iter);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
|
// Inserta un hijo después del componente con el `id` dado, o al final si no se encuentra.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn insert_after_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
fn insert_after_id(&mut self, id: impl AsRef<str>, child: Child) -> &mut Self {
|
||||||
|
|
@ -275,6 +347,17 @@ impl Children {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inserta más de un componente hijo al principio de la lista (manteniendo el orden recibido).
|
||||||
|
#[inline]
|
||||||
|
fn prepend_many<I>(&mut self, iter: I) -> &mut Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Child>,
|
||||||
|
{
|
||||||
|
let buf: Vec<Child> = iter.into_iter().collect();
|
||||||
|
self.0.splice(0..0, buf);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
// Elimina el primer hijo con el `id` dado.
|
// Elimina el primer hijo con el `id` dado.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn remove_by_id(&mut self, id: impl AsRef<str>) -> &mut Self {
|
fn remove_by_id(&mut self, id: impl AsRef<str>) -> &mut Self {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue