✨ [bootsier] Añade más componentes y repasa código
- Se incorpora nuevo componente Dropdown. - Se crea un componente Navbar con soporte para marca, elementos de navegación. - Se implementa el componente Offcanvas con opciones de posición, visibilidad y fondo personalizables. - Mejora el manejo de imágenes con un nuevo componente de Image. - Se reorganizan los componentes del tema para una mejor estructura y usabilidad.
This commit is contained in:
parent
cf40af13bb
commit
82837c622e
29 changed files with 2608 additions and 9 deletions
104
extensions/pagetop-bootsier/src/theme/navbar/brand.rs
Normal file
104
extensions/pagetop-bootsier/src/theme/navbar/brand.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Brand {
|
||||
id : AttrId,
|
||||
#[default(_code = "global::SETTINGS.app.name.to_owned()")]
|
||||
app_name : String,
|
||||
slogan : AttrL10n,
|
||||
logo : Typed<Image>,
|
||||
#[default(_code = "|_| \"/\"")]
|
||||
home : FnPathByContext,
|
||||
}
|
||||
|
||||
impl Component for Brand {
|
||||
fn new() -> Self {
|
||||
Brand::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let logo = self.logo().render(cx);
|
||||
let home = self.home()(cx);
|
||||
let title = &L10n::l("site_home").lookup(cx);
|
||||
PrepareMarkup::With(html! {
|
||||
div id=[self.id()] class="branding__container" {
|
||||
div class="branding__content" {
|
||||
@if !logo.is_empty() {
|
||||
a class="branding__logo" href=(home) title=[title] rel="home" {
|
||||
(logo)
|
||||
}
|
||||
}
|
||||
div class="branding__text" {
|
||||
a class="branding__name" href=(home) title=[title] rel="home" {
|
||||
(self.app_name())
|
||||
}
|
||||
@if let Some(slogan) = self.slogan().lookup(cx) {
|
||||
div class="branding__slogan" {
|
||||
(slogan)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Brand {
|
||||
// Brand 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_app_name(mut self, app_name: impl Into<String>) -> Self {
|
||||
self.app_name = app_name.into();
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_slogan(mut self, slogan: L10n) -> Self {
|
||||
self.slogan.alter_value(slogan);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_logo(mut self, logo: Option<Image>) -> Self {
|
||||
self.logo.alter_component(logo);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_home(mut self, home: FnPathByContext) -> Self {
|
||||
self.home = home;
|
||||
self
|
||||
}
|
||||
|
||||
// Brand GETTERS.
|
||||
|
||||
pub fn app_name(&self) -> &String {
|
||||
&self.app_name
|
||||
}
|
||||
|
||||
pub fn slogan(&self) -> &AttrL10n {
|
||||
&self.slogan
|
||||
}
|
||||
|
||||
pub fn logo(&self) -> &Typed<Image> {
|
||||
&self.logo
|
||||
}
|
||||
|
||||
pub fn home(&self) -> &FnPathByContext {
|
||||
&self.home
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(AutoDefault, PartialEq)]
|
||||
pub(crate) enum Toggle {
|
||||
#[default]
|
||||
Collapse,
|
||||
Offcanvas,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl fmt::Display for Toggle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Toggle::Collapse => write!(f, "collapse"),
|
||||
Toggle::Offcanvas => write!(f, "offcanvas"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
pub struct ButtonToggler;
|
||||
|
||||
impl Component for ButtonToggler {
|
||||
fn new() -> Self {
|
||||
ButtonToggler::default()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, _cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::With(html! {
|
||||
button
|
||||
type="button"
|
||||
class="navbar-toggler"
|
||||
{
|
||||
span class="navbar-toggler-icon" {}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonToggler {
|
||||
// ButtonToggler PRIVATE RENDER.
|
||||
|
||||
pub(crate) fn render(
|
||||
&self,
|
||||
cx: &mut Context,
|
||||
id_content: String,
|
||||
data_bs_toggle: Toggle,
|
||||
) -> Markup {
|
||||
let id_content_target = join!("#", id_content);
|
||||
let aria_expanded = if data_bs_toggle == Toggle::Collapse {
|
||||
Some("false")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
html! {
|
||||
button
|
||||
type="button"
|
||||
class="navbar-toggler"
|
||||
data-bs-toggle=(data_bs_toggle)
|
||||
data-bs-target=(id_content_target)
|
||||
aria-controls=(id_content)
|
||||
aria-expanded=[aria_expanded]
|
||||
aria-label=[L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx)]
|
||||
{
|
||||
span class="navbar-toggler-icon" {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
233
extensions/pagetop-bootsier/src/theme/navbar/component.rs
Normal file
233
extensions/pagetop-bootsier/src/theme/navbar/component.rs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
const TOGGLE_COLLAPSE: &str = "collapse";
|
||||
const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
pub enum NavbarToggler {
|
||||
#[default]
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
pub enum NavbarType {
|
||||
#[default]
|
||||
None,
|
||||
Nav(Typed<navbar::Nav>),
|
||||
Offcanvas(Typed<Offcanvas>),
|
||||
Text(L10n),
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Navbar {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
expand : BreakPoint,
|
||||
toggler : NavbarToggler,
|
||||
navbar_type: NavbarType,
|
||||
contents : Children,
|
||||
brand : Typed<navbar::Brand>,
|
||||
}
|
||||
|
||||
impl Component for Navbar {
|
||||
fn new() -> Self {
|
||||
Navbar::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(
|
||||
ClassesOp::Prepend,
|
||||
[
|
||||
"navbar".to_string(),
|
||||
self.expand().try_class("navbar-expand").unwrap_or_default(),
|
||||
]
|
||||
.join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
let id = cx.required_id::<Self>(self.id());
|
||||
|
||||
let navbar_type = match self.navbar_type() {
|
||||
NavbarType::None => return PrepareMarkup::None,
|
||||
NavbarType::Nav(nav) => {
|
||||
let id_content = join!(id, "-content");
|
||||
match self.toggler() {
|
||||
NavbarToggler::Enabled => self.toggler_wrapper(
|
||||
TOGGLE_COLLAPSE,
|
||||
L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx),
|
||||
id_content,
|
||||
self.brand().render(cx),
|
||||
nav.render(cx),
|
||||
),
|
||||
NavbarToggler::Disabled => nav.render(cx),
|
||||
}
|
||||
}
|
||||
NavbarType::Offcanvas(oc) => {
|
||||
let id_content = oc.id().unwrap_or_default();
|
||||
self.toggler_wrapper(
|
||||
TOGGLE_OFFCANVAS,
|
||||
L10n::t("toggle", &LOCALES_BOOTSIER).lookup(cx),
|
||||
id_content,
|
||||
self.brand().render(cx),
|
||||
oc.render(cx),
|
||||
)
|
||||
}
|
||||
NavbarType::Text(text) => html! {
|
||||
span class="navbar-text" {
|
||||
(text.using(cx))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
self.nav_wrapper(id, self.brand().render(cx), navbar_type)
|
||||
}
|
||||
}
|
||||
|
||||
impl Navbar {
|
||||
pub fn with_nav(nav: navbar::Nav) -> Self {
|
||||
Navbar::default().with_navbar_type(NavbarType::Nav(Typed::with(nav)))
|
||||
}
|
||||
|
||||
pub fn with_offcanvas(offcanvas: Offcanvas) -> Self {
|
||||
Navbar::default().with_navbar_type(NavbarType::Offcanvas(Typed::with(offcanvas)))
|
||||
}
|
||||
|
||||
// Navbar 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
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_expand(mut self, bp: BreakPoint) -> Self {
|
||||
self.expand = bp;
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_toggler(mut self, toggler: NavbarToggler) -> Self {
|
||||
self.toggler = toggler;
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_navbar_type(mut self, navbar_type: NavbarType) -> Self {
|
||||
self.navbar_type = navbar_type;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_content(mut self, content: navbar::Content) -> Self {
|
||||
self.contents.add(Child::with(content));
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_contents(mut self, op: TypedOp<navbar::Content>) -> Self {
|
||||
self.contents.alter_typed(op);
|
||||
self
|
||||
}
|
||||
|
||||
#[builder_fn]
|
||||
pub fn with_brand(mut self, brand: Option<navbar::Brand>) -> Self {
|
||||
self.brand.alter_component(brand);
|
||||
self
|
||||
}
|
||||
|
||||
// Navbar GETTERS.
|
||||
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
pub fn expand(&self) -> &BreakPoint {
|
||||
&self.expand
|
||||
}
|
||||
|
||||
pub fn toggler(&self) -> &NavbarToggler {
|
||||
&self.toggler
|
||||
}
|
||||
|
||||
pub fn navbar_type(&self) -> &NavbarType {
|
||||
&self.navbar_type
|
||||
}
|
||||
|
||||
pub fn contents(&self) -> &Children {
|
||||
&self.contents
|
||||
}
|
||||
|
||||
pub fn brand(&self) -> &Typed<navbar::Brand> {
|
||||
&self.brand
|
||||
}
|
||||
|
||||
// Navbar HELPERS.
|
||||
|
||||
fn nav_wrapper(&self, id: String, brand: Markup, content: Markup) -> PrepareMarkup {
|
||||
if content.is_empty() {
|
||||
PrepareMarkup::None
|
||||
} else {
|
||||
PrepareMarkup::With(html! {
|
||||
(brand)
|
||||
nav id=(id) class=[self.classes().get()] {
|
||||
div class="container-fluid" {
|
||||
(content)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn toggler_wrapper(
|
||||
&self,
|
||||
data_bs_toggle: &str,
|
||||
aria_label: Option<String>,
|
||||
id_content: String,
|
||||
brand: Markup,
|
||||
content: Markup,
|
||||
) -> Markup {
|
||||
if content.is_empty() {
|
||||
html! {}
|
||||
} else {
|
||||
let id_content_target = join!("#", id_content);
|
||||
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
|
||||
Some("false")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
html! {
|
||||
(brand)
|
||||
button
|
||||
type="button"
|
||||
class="navbar-toggler"
|
||||
data-bs-toggle=(data_bs_toggle)
|
||||
data-bs-target=(id_content_target)
|
||||
aria-controls=(id_content)
|
||||
aria-expanded=[aria_expanded]
|
||||
aria-label=[aria_label]
|
||||
{
|
||||
span class="navbar-toggler-icon" {}
|
||||
}
|
||||
div id=(id_content) class="collapse navbar-collapse" {
|
||||
(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
69
extensions/pagetop-bootsier/src/theme/navbar/content.rs
Normal file
69
extensions/pagetop-bootsier/src/theme/navbar/content.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::navbar;
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
pub enum ContentType {
|
||||
#[default]
|
||||
None,
|
||||
Brand(Typed<navbar::Brand>),
|
||||
Nav(Typed<navbar::Nav>),
|
||||
Text(L10n),
|
||||
}
|
||||
|
||||
// Item.
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
pub struct Content {
|
||||
content: ContentType,
|
||||
}
|
||||
|
||||
impl Component for Content {
|
||||
fn new() -> Self {
|
||||
Content::default()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.content() {
|
||||
ContentType::None => PrepareMarkup::None,
|
||||
ContentType::Brand(brand) => PrepareMarkup::With(html! {
|
||||
(brand.render(cx))
|
||||
}),
|
||||
ContentType::Nav(nav) => PrepareMarkup::With(html! {
|
||||
(nav.render(cx))
|
||||
}),
|
||||
ContentType::Text(text) => PrepareMarkup::With(html! {
|
||||
span class="navbar-text" {
|
||||
(text.using(cx))
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn brand(content: navbar::Brand) -> Self {
|
||||
Content {
|
||||
content: ContentType::Brand(Typed::with(content)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nav(content: navbar::Nav) -> Self {
|
||||
Content {
|
||||
content: ContentType::Nav(Typed::with(content)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(content: L10n) -> Self {
|
||||
Content {
|
||||
content: ContentType::Text(content),
|
||||
}
|
||||
}
|
||||
|
||||
// Content GETTERS.
|
||||
|
||||
pub fn content(&self) -> &ContentType {
|
||||
&self.content
|
||||
}
|
||||
}
|
||||
113
extensions/pagetop-bootsier/src/theme/navbar/item.rs
Normal file
113
extensions/pagetop-bootsier/src/theme/navbar/item.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
75
extensions/pagetop-bootsier/src/theme/navbar/nav.rs
Normal file
75
extensions/pagetop-bootsier/src/theme/navbar/nav.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue