Preparando liberación de la versión 0.5 de PageTop #10
178 changed files with 11596 additions and 4947 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -8,3 +8,5 @@
|
|||
**/local.*.toml
|
||||
**/local.toml
|
||||
.env
|
||||
.cargo
|
||||
.vscode
|
||||
|
|
|
|||
130
CONTRIBUTING.md
Normal file
130
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Guía de contribución a PageTop
|
||||
|
||||
Gracias por tu interés en contribuir a **PageTop** 🎉
|
||||
|
||||
Este documento describe **cómo participar en el desarrollo del proyecto**, el flujo de trabajo y las
|
||||
normas que permitan garantizar un historial limpio, trazable y sostenible a largo plazo.
|
||||
|
||||
Por favor, léelo completo antes de abrir una *issue* o una *pull request*.
|
||||
|
||||
|
||||
## 1. Repositorios
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
> ⚠️ **Importante**
|
||||
> Aunque GitHub permite abrir *issues* y *pull requests*, **la integración del código se realiza
|
||||
> únicamente en el repositorio oficial**. GitHub actúa como repositorio espejo que se sincroniza
|
||||
> automáticamente para reflejar el mismo estado.
|
||||
|
||||
|
||||
## 2. Issues (incidencias, propuestas, preguntas)
|
||||
|
||||
Antes de abrir una *issue* **en GitHub**:
|
||||
|
||||
* comprueba que no exista ya una similar,
|
||||
* describe claramente el problema o propuesta,
|
||||
* incluye pasos de reproducción si se trata de un *bug*,
|
||||
* indica versión, entorno y contexto cuando sea relevante.
|
||||
|
||||
Las *issues* se usan para:
|
||||
|
||||
* informar de errores,
|
||||
* propuestas de mejora,
|
||||
* discusión técnica previa a cambios relevantes.
|
||||
|
||||
|
||||
## 3. Pull Requests (PRs)
|
||||
|
||||
### 3.1 Dónde abrirlas
|
||||
|
||||
Las *pull requests* se abren **en GitHub**, normalmente contra la rama `main`. GitHub es el punto de
|
||||
entrada recomendado para contribuciones externas.
|
||||
|
||||
### 3.2 Reglas generales para PRs
|
||||
|
||||
* Cada PR debe abordar **un único objetivo claro**.
|
||||
* Mantén el alcance lo más acotado posible.
|
||||
* Incluye descripción clara del cambio.
|
||||
* Si el PR corrige una *issue*, enlázala explícitamente.
|
||||
* Asegúrate de que el código compila y pasa las pruebas.
|
||||
|
||||
### 3.3 Revisión y aceptación
|
||||
|
||||
Todas las PRs son **revisadas manualmente** y pueden recibir comentarios o solicitudes de cambios.
|
||||
|
||||
Las PRs aceptadas se integran en el repositorio oficial, nunca directamente en GitHub, preservando
|
||||
siempre la **autoría original** del contribuidor.
|
||||
|
||||
|
||||
### 3.4. Cierre de Pull Requests y sincronización
|
||||
|
||||
Una vez que el cambio ha sido integrado en el repositorio oficial:
|
||||
|
||||
* La PR en GitHub se **cierra manualmente**.
|
||||
* Se añade un **mensaje estándar de cierre** indicando que el cambio ha sido integrado.
|
||||
* El repositorio de GitHub **se sincroniza automáticamente** como espejo.
|
||||
|
||||
|
||||
## 4. Estilo de código y calidad
|
||||
|
||||
* Sigue el estilo existente del proyecto.
|
||||
* Mantén los comentarios claros y precisos.
|
||||
* La documentación es parte del código: actualízala cuando sea necesario.
|
||||
* Cambios públicos o estructurales deben ir acompañados de documentación.
|
||||
|
||||
|
||||
## 5. Commits
|
||||
|
||||
PageTop usa la especificación **gitmoji** para los mensajes de *commit*. El formato recomendado es:
|
||||
|
||||
```text
|
||||
<propósito> [(ámbito opcional):] <mensaje>
|
||||
«LÍNEA EN BLANCO»
|
||||
Cuerpo opcional
|
||||
«LÍNEA EN BLANCO»
|
||||
Nota(s) al pie opcional(es) para referencias, incidencias o cambios incompatibles
|
||||
```
|
||||
|
||||
Ejemplos (no más de 50 caracteres en la primera línea, y no más de 80 en el resto):
|
||||
|
||||
* `📝 Actualiza la guía de contribución`
|
||||
* `♻️ (locale): Refactoriza sistema de localización`
|
||||
* Un mensaje completo:
|
||||
```
|
||||
🎨 (bootsier): Mejora la asignación de clases
|
||||
|
||||
- Simplifica la generación de clases CSS para componentes Bootstrap.
|
||||
- Elimina duplicidades en enums de estilos y centraliza la lógica de composición
|
||||
para reducir errores y facilitar mantenimiento.
|
||||
- Alinea los nombres de variantes con la documentación pública.
|
||||
|
||||
Refs: PR #123
|
||||
```
|
||||
|
||||
El emoji puede usarse en formato Unicode o como *shortcode*, por ejemplo `:sparkles:` en vez de ✨.
|
||||
|
||||
Consulta la especificación oficial en https://gitmoji.dev/specification
|
||||
|
||||
Durante la integración, los *commits* pueden ajustarse para adaptarse al historial del proyecto.
|
||||
|
||||
Un *commit* debe representar una unidad lógica de cambio. Usa mensajes claros y descriptivos.
|
||||
|
||||
|
||||
## 6. Comunicación y respeto
|
||||
|
||||
PageTop sigue un enfoque profesional y colaborativo:
|
||||
|
||||
* Sé respetuoso en revisiones y discusiones.
|
||||
* Acepta sugerencias técnicas como parte del proceso.
|
||||
* Recuerda que todas las contribuciones son revisadas con el objetivo de mejorar el proyecto.
|
||||
|
||||
Si tienes dudas sobre el proceso, abre una *issue* de tipo pregunta para tratar la cuestión en
|
||||
comunidad.
|
||||
|
||||
---
|
||||
|
||||
Gracias por contribuir a **PageTop** 🚀 Cada aportación contribuye a mejorar el proyecto.
|
||||
13
CREDITS.md
13
CREDITS.md
|
|
@ -1,7 +1,7 @@
|
|||
# 🔃 Dependencias
|
||||
|
||||
PageTop está basado en [Rust](https://www.rust-lang.org/) y crece a hombros de gigantes aprovechando
|
||||
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs) como son:
|
||||
algunas de las librerías más robustas y populares del [ecosistema Rust](https://lib.rs/) como son:
|
||||
|
||||
* [Actix Web](https://actix.rs/) para los servicios web.
|
||||
* [Config](https://docs.rs/config) para cargar y procesar las opciones de configuración.
|
||||
|
|
@ -26,8 +26,15 @@ para mostrar un banner de presentación en el terminal con el nombre de la aplic
|
|||
* [starwars.flf](http://www.figlet.org/fontdb_example.cgi?font=starwars.flf) de *Ryan Youck*
|
||||
|
||||
|
||||
# 🎨 Icono
|
||||
# 🎨 CSS
|
||||
|
||||
"La Criatura" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
La extensión `pagetop-bootsier` es un tema que integra [Bootstrap 5.3.8](https://getbootstrap.com/)
|
||||
para los estilos y componentes de la interfaz. Bootstrap está distribuido bajo licencia
|
||||
[MIT](https://github.com/twbs/bootstrap/blob/main/LICENSE).
|
||||
|
||||
|
||||
# 👾 Icono
|
||||
|
||||
"La Mascota" sonriente es una simpática creación de [Webalys](https://www.iconfinder.com/webalys).
|
||||
Forma parte de su colección [Nasty Icons](https://www.iconfinder.com/iconsets/nasty), disponible en
|
||||
[ICONFINDER](https://www.iconfinder.com).
|
||||
|
|
|
|||
1235
Cargo.lock
generated
1235
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
18
Cargo.toml
18
Cargo.toml
|
|
@ -16,14 +16,13 @@ authors.workspace = true
|
|||
|
||||
[dependencies]
|
||||
chrono = "0.4"
|
||||
colored = "3.0"
|
||||
concat-string = "1.0"
|
||||
colored = "3.1"
|
||||
config = { version = "0.15", default-features = false, features = ["toml"] }
|
||||
figlet-rs = "0.1"
|
||||
indoc = "2.0"
|
||||
figlet-rs = "1.0"
|
||||
getter-methods = "2.0"
|
||||
itoa = "1.0"
|
||||
indexmap = "2.14"
|
||||
parking_lot = "0.12"
|
||||
paste = { package = "pastey", version = "0.1" }
|
||||
substring = "1.4"
|
||||
terminal_size = "0.4"
|
||||
|
||||
|
|
@ -32,7 +31,7 @@ tracing-appender = "0.2"
|
|||
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
||||
tracing-actix-web = "0.7"
|
||||
|
||||
fluent-templates = "0.13"
|
||||
fluent-templates = "0.14"
|
||||
unic-langid = { version = "0.9", features = ["macros"] }
|
||||
|
||||
actix-web = { workspace = true, default-features = true }
|
||||
|
|
@ -42,6 +41,7 @@ actix-web-files = { package = "actix-files", version = "0.6" }
|
|||
serde.workspace = true
|
||||
|
||||
pagetop-macros.workspace = true
|
||||
pagetop-minimal.workspace = true
|
||||
pagetop-statics.workspace = true
|
||||
|
||||
[features]
|
||||
|
|
@ -49,7 +49,7 @@ default = []
|
|||
testing = []
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.23"
|
||||
tempfile = "3.27"
|
||||
serde_json = "1.0"
|
||||
pagetop-aliner.workspace = true
|
||||
pagetop-bootsier.workspace = true
|
||||
|
|
@ -64,6 +64,7 @@ members = [
|
|||
# Helpers
|
||||
"helpers/pagetop-build",
|
||||
"helpers/pagetop-macros",
|
||||
"helpers/pagetop-minimal",
|
||||
"helpers/pagetop-statics",
|
||||
# Extensions
|
||||
"extensions/pagetop-aliner",
|
||||
|
|
@ -77,11 +78,12 @@ license = "MIT OR Apache-2.0"
|
|||
authors = ["Manuel Cillero <manuel@cillero.es>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
actix-web = { version = "4.11", default-features = false }
|
||||
actix-web = { version = "4.13", default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
# Helpers
|
||||
pagetop-build = { version = "0.3", path = "helpers/pagetop-build" }
|
||||
pagetop-macros = { version = "0.2", path = "helpers/pagetop-macros" }
|
||||
pagetop-minimal = { version = "0.0", path = "helpers/pagetop-minimal" }
|
||||
pagetop-statics = { version = "0.1", path = "helpers/pagetop-statics" }
|
||||
# Extensions
|
||||
pagetop-aliner = { version = "0.0", path = "extensions/pagetop-aliner" }
|
||||
|
|
|
|||
156
MAINTAINERS.md
Normal file
156
MAINTAINERS.md
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
# MAINTAINERS.md
|
||||
|
||||
## Guía para mantenedores de PageTop
|
||||
|
||||
Este documento describe **el flujo técnico interno de revisión e integración de contribuciones** en
|
||||
**PageTop**.
|
||||
|
||||
Está dirigido a **mantenedores del proyecto** y **no forma parte de la guía de contribución para
|
||||
usuarios externos**. Su objetivo es servir como **referencia operativa**, garantizando coherencia,
|
||||
trazabilidad y preservación de la autoría en un entorno con repositorios espejo.
|
||||
|
||||
|
||||
## 1. Repositorios y principios
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
### Principios clave
|
||||
|
||||
* El repositorio oficial **es la única fuente de verdad** del historial.
|
||||
* **Nunca se realizan *merges* en GitHub**.
|
||||
* Toda integración definitiva se realiza en el repositorio oficial.
|
||||
* La autoría original debe preservarse siempre.
|
||||
|
||||
|
||||
## 2. Configuración local recomendada
|
||||
|
||||
El remoto `github` debe configurarse únicamente para operaciones de lectura (*fetch*), con la URL de
|
||||
*push* deshabilitada para evitar publicaciones accidentales en el repositorio espejo.
|
||||
|
||||
Estado esperado de `git remote -v`:
|
||||
|
||||
```text
|
||||
origin git@git.cillero.es:manuelcillero/pagetop.git (fetch)
|
||||
origin git@git.cillero.es:manuelcillero/pagetop.git (push)
|
||||
github git@github.com:manuelcillero/pagetop.git (fetch)
|
||||
github DISABLED (push)
|
||||
```
|
||||
|
||||
Convenciones usadas en este documento:
|
||||
|
||||
* `origin` -> Repositorio oficial
|
||||
* `github` -> Repositorio espejo
|
||||
|
||||
|
||||
## 3. Recepción y revisión de Pull Requests
|
||||
|
||||
Las PRs externas llegan por GitHub, normalmente contra la rama `main`.
|
||||
|
||||
Se asume que el repositorio local está configurado para recuperar PRs de GitHub como referencias
|
||||
remotas (`refs/pull/<N>/head`):
|
||||
|
||||
```bash
|
||||
git fetch github --prune
|
||||
git checkout -b pr-123 github/pr/123
|
||||
```
|
||||
|
||||
Antes de integrar:
|
||||
|
||||
* Revisar el código manualmente.
|
||||
* Verificar formato, análisis y pruebas:
|
||||
|
||||
```bash
|
||||
cargo fmt
|
||||
cargo clippy
|
||||
cargo test
|
||||
```
|
||||
|
||||
* Comprobar impacto en documentación.
|
||||
* Evaluar coherencia con la arquitectura y el estilo del proyecto.
|
||||
|
||||
Los cambios adicionales se solicitan o se aplican explicando claramente el motivo.
|
||||
|
||||
|
||||
## 4. Estrategia de integración
|
||||
|
||||
La integración **se realiza siempre en el repositorio oficial** (`origin`).
|
||||
|
||||
### 4.1 Estrategia por defecto: *rebase* + *fast-forward*
|
||||
|
||||
Esta es la **estrategia estándar y recomendada** en PageTop. Ventajas:
|
||||
|
||||
* conserva los commits originales,
|
||||
* preserva la autoría real de cada cambio,
|
||||
* mantiene un historial lineal y trazable,
|
||||
* facilita auditoría y depuración.
|
||||
|
||||
Procedimiento típico:
|
||||
|
||||
```bash
|
||||
git checkout pr-123
|
||||
git rebase main
|
||||
|
||||
# Resolver conflictos si los hay
|
||||
|
||||
git checkout main
|
||||
git merge --ff-only pr-123
|
||||
```
|
||||
|
||||
Si `merge --ff-only` falla, **no se debe continuar**, indica divergencias que deben resolverse antes
|
||||
de integrar la PR.
|
||||
|
||||
### 4.2 Estrategia excepcional: *Squash*
|
||||
|
||||
Sólo debe usarse cuando esté justificado:
|
||||
|
||||
* la PR contiene múltiples commits de prueba o ruido,
|
||||
* el historial aportado no es significativo,
|
||||
* el cambio es pequeño y autocontenido.
|
||||
|
||||
En este caso, se debe **preservar explícitamente la autoría**:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git merge --squash pr-123
|
||||
git commit --author="Nombre Apellido <email@ejemplo.com>"
|
||||
```
|
||||
|
||||
|
||||
### 4.3. Publicación en el repositorio oficial
|
||||
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
Este *push* representa la **integración definitiva** del cambio en la rama `main`.
|
||||
|
||||
|
||||
## 5. Cierre de la PR y sincronización
|
||||
|
||||
Tras integrar el cambio en el repositorio oficial, se cierra manualmente la PR en GitHub con un
|
||||
mensaje estándar:
|
||||
|
||||
```text
|
||||
Gracias por tu contribución.
|
||||
|
||||
Este cambio ha sido integrado en el repositorio oficial en `main` (`<hash>`).
|
||||
GitHub actúa como repositorio espejo sincronizado.
|
||||
```
|
||||
|
||||
|
||||
## 6. Principios de mantenimiento
|
||||
|
||||
* Priorizar **claridad y trazabilidad** frente a rapidez.
|
||||
* Mantener un historial legible y significativo.
|
||||
* Documentar cambios estructurales o públicos.
|
||||
* Tratar las contribuciones externas con respeto y transparencia.
|
||||
|
||||
---
|
||||
|
||||
Este documento puede evolucionar con el proyecto.
|
||||
|
||||
No se trata de imponer rigidez, sino de **capturar el conocimiento operativo real** de PageTop como
|
||||
guía práctica para el mantenimiento.
|
||||
55
README.md
55
README.md
|
|
@ -26,10 +26,10 @@ según las necesidades de cada proyecto, incluyendo:
|
|||
* **Extensiones** (*extensions*): añaden, extienden o personalizan funcionalidades usando las APIs
|
||||
de PageTop o de terceros.
|
||||
* **Temas** (*themes*): son extensiones que permiten modificar la apariencia de páginas y
|
||||
componentes sin comprometer su funcionalidad.
|
||||
componentes.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
La aplicación más sencilla de PageTop se ve así:
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ Este programa implementa una extensión llamada `HelloWorld` que sirve una pági
|
|||
(`/`) mostrando el texto "Hello world!" dentro de un elemento HTML `<h1>`.
|
||||
|
||||
|
||||
# 📂 Repositorio
|
||||
## 📂 Proyecto
|
||||
|
||||
El código se organiza en un *workspace* donde actualmente se incluyen los siguientes subproyectos:
|
||||
|
||||
|
|
@ -82,21 +82,25 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
|
|||
fuente de la librería principal. Reúne algunos de los *crates* más estables y populares del
|
||||
ecosistema Rust para proporcionar APIs y recursos para la creación avanzada de soluciones web.
|
||||
|
||||
## Auxiliares
|
||||
|
||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
||||
es la librería que permite incluir archivos estáticos en el ejecutable de las aplicaciones
|
||||
PageTop para servirlos de forma eficiente, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
### Auxiliares
|
||||
|
||||
* **[pagetop-build](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-build)**,
|
||||
prepara los archivos estáticos o archivos SCSS compilados para incluirlos en el binario de las
|
||||
aplicaciones PageTop durante la compilación de los ejecutables.
|
||||
|
||||
* **[pagetop-macros](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-macros)**,
|
||||
proporciona una colección de macros que mejoran la experiencia de desarrollo con PageTop.
|
||||
proporciona una colección de macros procedurales que mejoran la experiencia de desarrollo con
|
||||
PageTop.
|
||||
|
||||
## Extensiones
|
||||
* **[pagetop-minimal](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal)**,
|
||||
ofrece macros declarativas esenciales para optimizar tareas comunes como la composición de
|
||||
texto, la concatenación de cadenas y el manejo de colecciones clave-valor.
|
||||
|
||||
* **[pagetop-statics](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-statics)**,
|
||||
permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para servirlos
|
||||
de forma eficiente, con detección de cambios que optimizan el tiempo de compilación.
|
||||
|
||||
### Extensiones
|
||||
|
||||
* **[pagetop-aliner](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/extensions/pagetop-aliner)**,
|
||||
es un tema para demos y pruebas que muestra esquemáticamente la composición de las páginas HTML.
|
||||
|
|
@ -106,7 +110,7 @@ El código se organiza en un *workspace* donde actualmente se incluyen los sigui
|
|||
componentes flexibles.
|
||||
|
||||
|
||||
# 🧪 Pruebas
|
||||
## 🧪 Pruebas
|
||||
|
||||
Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de Cargo** declarados en
|
||||
`.cargo/config.toml`. Basta con ejecutarlos desde la raíz del proyecto:
|
||||
|
|
@ -123,14 +127,14 @@ Para simplificar el flujo de trabajo, el repositorio incluye varios **alias de C
|
|||
> Si quieres **activar** las trazas del registro de eventos entonces usa simplemente `cargo test`.
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
@ -144,7 +148,28 @@ Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el es
|
|||
el ecosistema Rust.
|
||||
|
||||
|
||||
# ✨ Contribuir
|
||||
## ✨ Contribuir
|
||||
|
||||
PageTop mantiene **un único repositorio oficial**:
|
||||
|
||||
* **Repositorio oficial:** https://git.cillero.es/manuelcillero/pagetop
|
||||
* **Repositorio espejo:** https://github.com/manuelcillero/pagetop
|
||||
|
||||
El repositorio de GitHub actúa como espejo y punto de entrada para:
|
||||
|
||||
* dar mayor visibilidad al proyecto,
|
||||
* facilitar la participación de la comunidad,
|
||||
* centralizar *issues* y *pull requests* externas.
|
||||
|
||||
Aunque GitHub permite abrir *pull requests*, **la integración del código se realiza únicamente en el
|
||||
repositorio oficial**. El repositorio de GitHub se sincroniza posteriormente para reflejar el mismo
|
||||
estado.
|
||||
|
||||
En todos los casos, se respeta la **autoría original** de las contribuciones integradas, tanto en el
|
||||
historial como en la documentación asociada al cambio.
|
||||
|
||||
Para conocer el proceso completo de participación, revisión e integración de cambios, consulta el
|
||||
archivo [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
|
||||
Cualquier contribución para añadir al proyecto se considerará automáticamente bajo la doble licencia
|
||||
indicada arriba (MIT o Apache v2.0), sin términos o condiciones adicionales, tal y como permite la
|
||||
|
|
|
|||
456
examples/form-controls.rs
Normal file
456
examples/form-controls.rs
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use pagetop_bootsier::prelude::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
struct FormControls;
|
||||
|
||||
impl Extension for FormControls {
|
||||
fn dependencies(&self) -> Vec<ExtensionRef> {
|
||||
vec![&pagetop_aliner::Aliner, &pagetop_bootsier::Bootsier]
|
||||
}
|
||||
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(form_controls));
|
||||
}
|
||||
}
|
||||
|
||||
async fn form_controls(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_child(
|
||||
Intro::default()
|
||||
.with_opening(IntroOpening::Custom)
|
||||
.with_title(L10n::t("title", &LOC))
|
||||
.with_slogan(L10n::t("slogan", &LOC))
|
||||
.with_button(None::<(L10n, FnPathByContext)>)
|
||||
// Bloque 1: casillas, interruptores y botones de opción.
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("block_selections", &LOC))
|
||||
.with_child(
|
||||
Form::new()
|
||||
.with_id("form-selections")
|
||||
.with_action("/")
|
||||
.with_method(form::Method::Post)
|
||||
// Casillas e interruptores (form::Checkbox).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_checkbox", &LOC))
|
||||
.with_description(L10n::t("desc_checkbox", &LOC))
|
||||
.with_child(
|
||||
form::Checkbox::new()
|
||||
.with_name("accept_terms")
|
||||
.with_label(L10n::t("label_terms", &LOC))
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Checkbox::new()
|
||||
.with_name("accept_marketing")
|
||||
.with_label(L10n::t("label_marketing", &LOC))
|
||||
.with_checked(true)
|
||||
.with_inline(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Checkbox::new()
|
||||
.with_name("newsletter")
|
||||
.with_label(L10n::t("label_newsletter", &LOC))
|
||||
.with_inline(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Checkbox::switch()
|
||||
.with_name("notifications")
|
||||
.with_label(L10n::t("label_notifications", &LOC))
|
||||
.with_checked(true)
|
||||
.with_reverse(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Checkbox::switch()
|
||||
.with_name("dark_mode")
|
||||
.with_label(L10n::t("label_dark_mode", &LOC))
|
||||
.with_disabled(true),
|
||||
),
|
||||
)
|
||||
// Grupo de casillas de verificación (form::check::Field).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_checkgroup", &LOC))
|
||||
.with_child(
|
||||
form::check::Field::new()
|
||||
.with_name("interests")
|
||||
.with_label(L10n::t("label_interests", &LOC))
|
||||
.with_help_text(L10n::t("help_interests", &LOC))
|
||||
.with_item(
|
||||
form::check::Item::new(
|
||||
"rust",
|
||||
L10n::t("check_rust", &LOC),
|
||||
)
|
||||
.with_checked(true),
|
||||
)
|
||||
.with_item(form::check::Item::new(
|
||||
"web",
|
||||
L10n::t("check_web", &LOC),
|
||||
))
|
||||
.with_item(form::check::Item::new(
|
||||
"ai",
|
||||
L10n::t("check_ai", &LOC),
|
||||
))
|
||||
.with_item(
|
||||
form::check::Item::new(
|
||||
"games",
|
||||
L10n::t("check_games", &LOC),
|
||||
)
|
||||
.with_disabled(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Botones de opción (form::radio::Field).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_radio", &LOC))
|
||||
.with_child(
|
||||
form::radio::Field::new()
|
||||
.with_name("frequency")
|
||||
.with_label(L10n::t("label_frequency", &LOC))
|
||||
.with_item(form::radio::Item::new(
|
||||
"daily",
|
||||
L10n::t("radio_daily", &LOC),
|
||||
))
|
||||
.with_item(
|
||||
form::radio::Item::new(
|
||||
"weekly",
|
||||
L10n::t("radio_weekly", &LOC),
|
||||
)
|
||||
.with_checked(true),
|
||||
)
|
||||
.with_item(form::radio::Item::new(
|
||||
"monthly",
|
||||
L10n::t("radio_monthly", &LOC),
|
||||
))
|
||||
.with_item(
|
||||
form::radio::Item::new(
|
||||
"never",
|
||||
L10n::t("radio_never", &LOC),
|
||||
)
|
||||
.with_disabled(true),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(
|
||||
form::Hidden::new()
|
||||
.with_name("origin")
|
||||
.with_value("form-selections"),
|
||||
)
|
||||
// Botones de acción.
|
||||
.with_child(
|
||||
Button::submit(L10n::t("btn_submit", &LOC))
|
||||
.with_color(ButtonColor::Background(Color::Primary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::reset(L10n::t("btn_reset", &LOC))
|
||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
||||
.with_color(ButtonColor::Link),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Bloque 2: campos de texto, multilínea y rango.
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("block_text", &LOC))
|
||||
.with_child(
|
||||
Form::new()
|
||||
.with_id("form-text")
|
||||
.with_action("/")
|
||||
.with_method(form::Method::Post)
|
||||
// Campos de texto (form::input::Field).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_text", &LOC))
|
||||
.with_child(
|
||||
form::input::Field::text()
|
||||
.with_name("name")
|
||||
.with_label(L10n::t("label_name", &LOC))
|
||||
.with_placeholder(L10n::t("placeholder_name", &LOC))
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::email()
|
||||
.with_name("email")
|
||||
.with_label(L10n::t("label_email", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_email",
|
||||
&LOC,
|
||||
))
|
||||
.with_autocomplete(
|
||||
Some(form::Autocomplete::email()),
|
||||
)
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::password()
|
||||
.with_name("password")
|
||||
.with_label(L10n::t("label_password", &LOC))
|
||||
.with_autocomplete(Some(
|
||||
form::Autocomplete::new_password(),
|
||||
))
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::telephone()
|
||||
.with_name("phone")
|
||||
.with_label(L10n::t("label_phone", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_phone",
|
||||
&LOC,
|
||||
)),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::url()
|
||||
.with_name("website")
|
||||
.with_label(L10n::t("label_url", &LOC))
|
||||
.with_placeholder(L10n::t("placeholder_url", &LOC)),
|
||||
)
|
||||
.with_child(
|
||||
form::input::Field::search()
|
||||
.with_name("search")
|
||||
.with_label(L10n::t("label_search", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_search",
|
||||
&LOC,
|
||||
)),
|
||||
),
|
||||
)
|
||||
// Área de texto (form::Textarea).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_textarea", &LOC))
|
||||
.with_child(
|
||||
form::Textarea::new()
|
||||
.with_name("comment")
|
||||
.with_label(L10n::t("label_comment", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_comment",
|
||||
&LOC,
|
||||
))
|
||||
.with_rows(Some(4))
|
||||
.with_help_text(L10n::t("help_comment", &LOC)),
|
||||
),
|
||||
)
|
||||
// Control deslizante (form::Range).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_range", &LOC))
|
||||
.with_child(
|
||||
form::Range::new()
|
||||
.with_name("rating")
|
||||
.with_label(L10n::t("label_rating", &LOC))
|
||||
.with_min(Some(1.0))
|
||||
.with_max(Some(10.0))
|
||||
.with_step(Some(1.0))
|
||||
.with_value(Some(5.0))
|
||||
.with_help_text(L10n::t("help_rating", &LOC)),
|
||||
),
|
||||
)
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(
|
||||
form::Hidden::new()
|
||||
.with_name("origin")
|
||||
.with_value("form-text"),
|
||||
)
|
||||
// Botones de acción.
|
||||
.with_child(
|
||||
Button::submit(L10n::t("btn_submit", &LOC))
|
||||
.with_color(ButtonColor::Background(Color::Primary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::reset(L10n::t("btn_reset", &LOC))
|
||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
||||
.with_color(ButtonColor::Link),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Bloque 3: listas de selección y etiquetas flotantes.
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("block_lists", &LOC))
|
||||
.with_child(
|
||||
Form::new()
|
||||
.with_id("form-lists")
|
||||
.with_action("/")
|
||||
.with_method(form::Method::Post)
|
||||
// Listas de selección (form::select::Field).
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_select", &LOC))
|
||||
.with_child(
|
||||
form::select::Field::new()
|
||||
.with_name("language")
|
||||
.with_label(L10n::t("label_language", &LOC))
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"",
|
||||
L10n::t("select_choose", &LOC),
|
||||
)
|
||||
.with_selected(true),
|
||||
)
|
||||
.with_group(
|
||||
form::select::Group::new(L10n::t(
|
||||
"select_group_europe",
|
||||
&LOC,
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"es",
|
||||
L10n::t("select_spanish", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"fr",
|
||||
L10n::t("select_french", &LOC),
|
||||
)),
|
||||
)
|
||||
.with_group(
|
||||
form::select::Group::new(L10n::t(
|
||||
"select_group_americas",
|
||||
&LOC,
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"en",
|
||||
L10n::t("select_english", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"pt",
|
||||
L10n::t("select_portuguese", &LOC),
|
||||
)),
|
||||
)
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"xx",
|
||||
L10n::t("select_disabled", &LOC),
|
||||
)
|
||||
.with_disabled(true),
|
||||
)
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::select::Field::new()
|
||||
.with_name("technologies")
|
||||
.with_label(L10n::t("label_technologies", &LOC))
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"rust",
|
||||
L10n::n("Rust"),
|
||||
)
|
||||
.with_selected(true),
|
||||
)
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"python",
|
||||
L10n::n("Python"),
|
||||
)
|
||||
.with_selected(true),
|
||||
)
|
||||
.with_item(form::select::Item::new(
|
||||
"javascript",
|
||||
L10n::n("JavaScript"),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"go",
|
||||
L10n::n("Go"),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"typescript",
|
||||
L10n::n("TypeScript"),
|
||||
))
|
||||
.with_multiple(true)
|
||||
.with_rows(Some(4))
|
||||
.with_help_text(L10n::t("help_technologies", &LOC)),
|
||||
),
|
||||
)
|
||||
// Etiquetas flotantes.
|
||||
.with_child(
|
||||
form::Fieldset::new()
|
||||
.with_legend(L10n::t("fieldset_floating", &LOC))
|
||||
.with_child(
|
||||
form::input::Field::text()
|
||||
.with_name("fl_name")
|
||||
.with_label(L10n::t("label_name", &LOC))
|
||||
.with_placeholder(L10n::t("placeholder_name", &LOC))
|
||||
.with_floating_label(true)
|
||||
.with_required(true),
|
||||
)
|
||||
.with_child(
|
||||
form::Textarea::new()
|
||||
.with_name("fl_comment")
|
||||
.with_label(L10n::t("label_comment", &LOC))
|
||||
.with_placeholder(L10n::t(
|
||||
"placeholder_comment",
|
||||
&LOC,
|
||||
))
|
||||
.with_floating_label(true),
|
||||
)
|
||||
.with_child(
|
||||
form::select::Field::new()
|
||||
.with_name("fl_country")
|
||||
.with_label(L10n::t("label_country", &LOC))
|
||||
.with_item(
|
||||
form::select::Item::new(
|
||||
"",
|
||||
L10n::t("select_choose", &LOC),
|
||||
)
|
||||
.with_selected(true),
|
||||
)
|
||||
.with_item(form::select::Item::new(
|
||||
"de",
|
||||
L10n::t("select_germany", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"es",
|
||||
L10n::t("select_spain", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"fr",
|
||||
L10n::t("select_france", &LOC),
|
||||
))
|
||||
.with_item(form::select::Item::new(
|
||||
"pt",
|
||||
L10n::t("select_portugal", &LOC),
|
||||
))
|
||||
.with_floating_label(true)
|
||||
.with_required(true),
|
||||
),
|
||||
)
|
||||
// Campo oculto (form::Hidden).
|
||||
.with_child(
|
||||
form::Hidden::new()
|
||||
.with_name("origin")
|
||||
.with_value("form-lists"),
|
||||
)
|
||||
// Botones de acción.
|
||||
.with_child(
|
||||
Button::submit(L10n::t("btn_submit", &LOC))
|
||||
.with_color(ButtonColor::Background(Color::Primary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::reset(L10n::t("btn_reset", &LOC))
|
||||
.with_color(ButtonColor::Outline(Color::Secondary)),
|
||||
)
|
||||
.with_child(
|
||||
Button::plain(L10n::t("btn_cancel", &LOC))
|
||||
.with_color(ButtonColor::Link),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&FormControls).run()?.await
|
||||
}
|
||||
|
|
@ -14,7 +14,11 @@ async fn hello_name(
|
|||
) -> ResultPage<Markup, ErrorPage> {
|
||||
let name = path.into_inner();
|
||||
Page::new(request)
|
||||
.add_child(Html::with(move |_| html! { h1 { "Hello " (name) "!" } }))
|
||||
.with_child(Html::with(move |_| {
|
||||
html! {
|
||||
h1 style="text-align: center;" { "Hello " (name) "!" }
|
||||
}
|
||||
}))
|
||||
.render()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ impl Extension for HelloWorld {
|
|||
|
||||
async fn hello_world(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.add_child(Html::with(|_| html! { h1 { "Hello World!" } }))
|
||||
.with_child(Html::with(|_| {
|
||||
html! {
|
||||
h1 style="text-align: center;" { "Hello World!" }
|
||||
}
|
||||
}))
|
||||
.render()
|
||||
}
|
||||
|
||||
|
|
|
|||
82
examples/intro-colors.rs
Normal file
82
examples/intro-colors.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
struct IntroColors;
|
||||
|
||||
impl Extension for IntroColors {
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
scfg.route("/", service::web::get().to(intro_colors));
|
||||
}
|
||||
}
|
||||
|
||||
async fn intro_colors(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_child(
|
||||
Intro::default()
|
||||
.with_opening(IntroOpening::Custom)
|
||||
.with_title(L10n::n("PageTop"))
|
||||
.with_slogan(L10n::t("colors_slogan", &LOC))
|
||||
.with_button(None::<(L10n, FnPathByContext)>)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "1"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_1", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "2"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_2", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "3"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_3", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "4"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_4", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "5"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_5", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::t("colors_block", &LOC).with_arg("n", "6"))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
p { (L10n::t("colors_val_6", &LOC).using(cx)) }
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.render()
|
||||
}
|
||||
|
||||
#[pagetop::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
Application::prepare(&IntroColors).run()?.await
|
||||
}
|
||||
74
examples/locale/en-US/form-controls.ftl
Normal file
74
examples/locale/en-US/form-controls.ftl
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
title = Form controls
|
||||
slogan = Bootsier form components showcase
|
||||
block_selections = Checkboxes, switches and radio buttons
|
||||
block_text = Text fields, multiline and range
|
||||
block_lists = Select lists and floating labels
|
||||
|
||||
fieldset_text = Text fields
|
||||
label_name = Full name
|
||||
placeholder_name = e.g.: Jane Smith
|
||||
label_email = Email address
|
||||
placeholder_email = user@example.com
|
||||
label_password = Password
|
||||
label_phone = Phone number
|
||||
placeholder_phone = +1 555 000 0000
|
||||
label_url = Website
|
||||
placeholder_url = https://example.com
|
||||
label_search = Search
|
||||
placeholder_search = Search term...
|
||||
|
||||
fieldset_textarea = Multiline text
|
||||
label_comment = Comment
|
||||
placeholder_comment = Write your comment here...
|
||||
help_comment = Maximum 500 characters.
|
||||
|
||||
fieldset_select = Selection lists
|
||||
label_language = Language
|
||||
label_country = Country
|
||||
label_technologies = Preferred technologies
|
||||
help_technologies = Hold Ctrl (or Cmd on Mac) to select multiple options.
|
||||
select_choose = — Choose an option —
|
||||
select_group_europe = Europe
|
||||
select_spanish = Spanish
|
||||
select_french = French
|
||||
select_group_americas = Americas
|
||||
select_english = English
|
||||
select_portuguese = Portuguese
|
||||
select_disabled = Not available
|
||||
select_germany = Germany
|
||||
select_spain = Spain
|
||||
select_france = France
|
||||
select_portugal = Portugal
|
||||
|
||||
fieldset_checkbox = Checkboxes and switches
|
||||
desc_checkbox = This group shows standard checkboxes, inline checkboxes, reverse-aligned options, and toggle switches for binary choices.
|
||||
label_terms = I accept the terms and conditions
|
||||
label_marketing = Commercial emails (inline)
|
||||
label_newsletter = Newsletter (inline)
|
||||
label_notifications = Enable notifications (reverse)
|
||||
label_dark_mode = Dark mode (unavailable)
|
||||
|
||||
fieldset_radio = Radio buttons
|
||||
label_frequency = Newsletter frequency
|
||||
radio_daily = Daily
|
||||
radio_weekly = Weekly
|
||||
radio_monthly = Monthly
|
||||
radio_never = Never (disabled)
|
||||
|
||||
fieldset_checkgroup = Checkbox group
|
||||
label_interests = Areas of interest
|
||||
help_interests = Select all options that apply.
|
||||
check_rust = Rust programming
|
||||
check_web = Web development
|
||||
check_ai = Artificial intelligence
|
||||
check_games = Game development (disabled)
|
||||
|
||||
fieldset_floating = Floating labels
|
||||
|
||||
fieldset_range = Slider
|
||||
label_rating = Overall rating
|
||||
help_rating = From 1 (very poor) to 10 (excellent).
|
||||
|
||||
btn_submit = Submit
|
||||
btn_reset = Reset
|
||||
btn_cancel = Cancel
|
||||
8
examples/locale/en-US/intro-colors.ftl
Normal file
8
examples/locale/en-US/intro-colors.ftl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
colors_slogan = Chromatic intro test
|
||||
colors_block = Block { $n } — intro-bg-block-{ $n }
|
||||
colors_val_1 = Background color: <code style="color: #8b2500">#FFB84B</code> — Amber gold.
|
||||
colors_val_2 = Background color: <code style="color: #8b2500">#FFC66F</code> — Light golden.
|
||||
colors_val_3 = Background color: <code style="color: #8b2500">#FFD493</code> — Pale golden.
|
||||
colors_val_4 = Background color: <code style="color: #8b2500">#FFE3B7</code> — Light peach.
|
||||
colors_val_5 = Background color: <code style="color: #8b2500">#FFF1DB</code> — Cream.
|
||||
colors_val_6 = Background color: <code style="color: #8b2500">#FFFFFF</code> — White.
|
||||
23
examples/locale/en-US/navbar-menus.ftl
Normal file
23
examples/locale/en-US/navbar-menus.ftl
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
menus_item_label = Label
|
||||
menus_item_link = Link
|
||||
menus_item_blank = External link
|
||||
menus_item_disabled = Disabled link
|
||||
|
||||
menus_test_title = Dropdown
|
||||
|
||||
menus_dev_header = Intro
|
||||
menus_dev_getting_started = Getting started
|
||||
menus_dev_guides = Development guides
|
||||
menus_dev_forum = Developers forum
|
||||
|
||||
menus_sdk_header = Software Development Kits
|
||||
menus_sdk_rust = SDKs Rust
|
||||
menus_sdk_js = SDKs JavaScript
|
||||
menus_sdk_python = SDKs Python
|
||||
|
||||
menus_plugin_header = Plugins
|
||||
menus_plugin_auth = Rust Plugin Auth
|
||||
menus_plugin_cache = Rust Plugin Cache
|
||||
|
||||
menus_item_sign_up = Sign up
|
||||
menus_item_login = Login
|
||||
74
examples/locale/es-ES/form-controls.ftl
Normal file
74
examples/locale/es-ES/form-controls.ftl
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
title = Controles de formulario
|
||||
slogan = Componentes Bootsier para formularios
|
||||
block_selections = Casillas, interruptores y botones de opción
|
||||
block_text = Campos de texto, multilínea y rango
|
||||
block_lists = Listas de selección y etiquetas flotantes
|
||||
|
||||
fieldset_text = Campos de texto
|
||||
label_name = Nombre completo
|
||||
placeholder_name = Ej.: Ana García
|
||||
label_email = Correo electrónico
|
||||
placeholder_email = usuario@ejemplo.com
|
||||
label_password = Contraseña
|
||||
label_phone = Teléfono
|
||||
placeholder_phone = +34 600 000
|
||||
label_url = Sitio web
|
||||
placeholder_url = https://ejemplo.com
|
||||
label_search = Búsqueda
|
||||
placeholder_search = Término de búsqueda...
|
||||
|
||||
fieldset_textarea = Texto multilínea
|
||||
label_comment = Comentario
|
||||
placeholder_comment = Escribe tu comentario aquí...
|
||||
help_comment = Máximo 500 caracteres.
|
||||
|
||||
fieldset_select = Listas de selección
|
||||
label_language = Idioma
|
||||
label_country = País
|
||||
label_technologies = Tecnologías preferidas
|
||||
help_technologies = Mantén Ctrl (o Cmd en Mac) para seleccionar varias opciones.
|
||||
select_choose = — Elige una opción —
|
||||
select_group_europe = Europa
|
||||
select_spanish = Español
|
||||
select_french = Francés
|
||||
select_group_americas = América
|
||||
select_english = Inglés
|
||||
select_portuguese = Portugués
|
||||
select_disabled = No disponible
|
||||
select_germany = Alemania
|
||||
select_spain = España
|
||||
select_france = Francia
|
||||
select_portugal = Portugal
|
||||
|
||||
fieldset_checkbox = Casillas e interruptores
|
||||
desc_checkbox = Este grupo muestra casillas de verificación estándar, casillas en línea, opciones alineadas a la derecha e interruptores para elecciones binarias.
|
||||
label_terms = Acepto los términos y condiciones
|
||||
label_marketing = Emails comerciales (en línea)
|
||||
label_newsletter = Boletín (en línea)
|
||||
label_notifications = Activar notificaciones (invertida)
|
||||
label_dark_mode = Modo oscuro (no disponible)
|
||||
|
||||
fieldset_radio = Botones de opción
|
||||
label_frequency = Frecuencia del boletín
|
||||
radio_daily = Diario
|
||||
radio_weekly = Semanal
|
||||
radio_monthly = Mensual
|
||||
radio_never = Nunca (deshabilitado)
|
||||
|
||||
fieldset_checkgroup = Grupo de casillas
|
||||
label_interests = Áreas de interés
|
||||
help_interests = Selecciona todas las opciones que correspondan.
|
||||
check_rust = Programación en Rust
|
||||
check_web = Desarrollo web
|
||||
check_ai = Inteligencia artificial
|
||||
check_games = Desarrollo de videojuegos (deshabilitado)
|
||||
|
||||
fieldset_floating = Etiquetas flotantes
|
||||
|
||||
fieldset_range = Control deslizante
|
||||
label_rating = Valoración general
|
||||
help_rating = De 1 (muy malo) a 10 (excelente).
|
||||
|
||||
btn_submit = Enviar
|
||||
btn_reset = Restablecer
|
||||
btn_cancel = Cancelar
|
||||
8
examples/locale/es-ES/intro-colors.ftl
Normal file
8
examples/locale/es-ES/intro-colors.ftl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
colors_slogan = Prueba de intro cromática
|
||||
colors_block = Bloque { $n } — intro-bg-block-{ $n }
|
||||
colors_val_1 = Color de fondo: <code style="color: #8b2500">#FFB84B</code> — Ámbar dorado.
|
||||
colors_val_2 = Color de fondo: <code style="color: #8b2500">#FFC66F</code> — Dorado claro.
|
||||
colors_val_3 = Color de fondo: <code style="color: #8b2500">#FFD493</code> — Dorado pálido.
|
||||
colors_val_4 = Color de fondo: <code style="color: #8b2500">#FFE3B7</code> — Melocotón claro.
|
||||
colors_val_5 = Color de fondo: <code style="color: #8b2500">#FFF1DB</code> — Crema.
|
||||
colors_val_6 = Color de fondo: <code style="color: #8b2500">#FFFFFF</code> — Blanco.
|
||||
23
examples/locale/es-ES/navbar-menus.ftl
Normal file
23
examples/locale/es-ES/navbar-menus.ftl
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
menus_item_label = Etiqueta
|
||||
menus_item_link = Enlace
|
||||
menus_item_blank = Enlace externo
|
||||
menus_item_disabled = Enlace deshabilitado
|
||||
|
||||
menus_test_title = Desplegable
|
||||
|
||||
menus_dev_header = Introducción
|
||||
menus_dev_getting_started = Primeros pasos
|
||||
menus_dev_guides = Guías de desarrollo
|
||||
menus_dev_forum = Foro de desarrolladores
|
||||
|
||||
menus_sdk_header = Kits de Desarrollo Software
|
||||
menus_sdk_rust = SDKs de Rust
|
||||
menus_sdk_js = SDKs de JavaScript
|
||||
menus_sdk_python = SDKs de Python
|
||||
|
||||
menus_plugin_header = Plugins
|
||||
menus_plugin_auth = Plugin Rust de autenticación
|
||||
menus_plugin_cache = Plugin Rust de caché
|
||||
|
||||
menus_item_sign_up = Registrarse
|
||||
menus_item_login = Iniciar sesión
|
||||
|
|
@ -2,6 +2,8 @@ use pagetop::prelude::*;
|
|||
|
||||
use pagetop_bootsier::prelude::*;
|
||||
|
||||
include_locales!(LOC from "examples/locale");
|
||||
|
||||
struct SuperMenu;
|
||||
|
||||
impl Extension for SuperMenu {
|
||||
|
|
@ -10,96 +12,87 @@ impl Extension for SuperMenu {
|
|||
}
|
||||
|
||||
fn initialize(&self) {
|
||||
let home_path = |cx: &Context| match cx.langid().language.as_str() {
|
||||
"en" => "/en",
|
||||
_ => "/",
|
||||
};
|
||||
|
||||
let navbar_menu = Navbar::brand_left(navbar::Brand::new().with_path(Some(home_path)))
|
||||
let navbar_menu = Navbar::brand_left(navbar::Brand::new())
|
||||
.with_expand(BreakPoint::LG)
|
||||
.add_item(navbar::Item::nav(
|
||||
.with_item(navbar::Item::nav(
|
||||
Nav::new()
|
||||
.add_item(nav::Item::link(
|
||||
L10n::l("sample_menus_item_link"),
|
||||
home_path,
|
||||
))
|
||||
.add_item(nav::Item::link_blank(
|
||||
L10n::l("sample_menus_item_blank"),
|
||||
|_| "https://docs.rs/pagetop",
|
||||
))
|
||||
.add_item(nav::Item::dropdown(
|
||||
Dropdown::new()
|
||||
.with_title(L10n::l("sample_menus_test_title"))
|
||||
.add_item(dropdown::Item::header(L10n::l("sample_menus_dev_header")))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_dev_getting_started"),
|
||||
|_| "/dev/getting-started",
|
||||
))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_dev_guides"),
|
||||
|_| "/dev/guides",
|
||||
))
|
||||
.add_item(dropdown::Item::link_blank(
|
||||
L10n::l("sample_menus_dev_forum"),
|
||||
|_| "https://forum.example.dev",
|
||||
))
|
||||
.add_item(dropdown::Item::divider())
|
||||
.add_item(dropdown::Item::header(L10n::l("sample_menus_sdk_header")))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_sdk_rust"),
|
||||
|_| "/dev/sdks/rust",
|
||||
))
|
||||
.add_item(dropdown::Item::link(L10n::l("sample_menus_sdk_js"), |_| {
|
||||
"/dev/sdks/js"
|
||||
.with_item(nav::Item::link(L10n::t("menus_item_link", &LOC), |cx| {
|
||||
cx.route("/")
|
||||
}))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_sdk_python"),
|
||||
|_| "/dev/sdks/python",
|
||||
.with_item(nav::Item::link_blank(
|
||||
L10n::t("menus_item_blank", &LOC),
|
||||
|_| "https://docs.rs/pagetop".into(),
|
||||
))
|
||||
.add_item(dropdown::Item::divider())
|
||||
.add_item(dropdown::Item::header(L10n::l(
|
||||
"sample_menus_plugin_header",
|
||||
)))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_plugin_auth"),
|
||||
|_| "/dev/sdks/rust/plugins/auth",
|
||||
.with_item(nav::Item::dropdown(
|
||||
Dropdown::new()
|
||||
.with_title(L10n::t("menus_test_title", &LOC))
|
||||
.with_item(dropdown::Item::header(L10n::t("menus_dev_header", &LOC)))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_dev_getting_started", &LOC),
|
||||
|cx| cx.route("/dev/getting-started"),
|
||||
))
|
||||
.add_item(dropdown::Item::link(
|
||||
L10n::l("sample_menus_plugin_cache"),
|
||||
|_| "/dev/sdks/rust/plugins/cache",
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_dev_guides", &LOC),
|
||||
|cx| cx.route("/dev/guides"),
|
||||
))
|
||||
.add_item(dropdown::Item::divider())
|
||||
.add_item(dropdown::Item::label(L10n::l("sample_menus_item_label")))
|
||||
.add_item(dropdown::Item::link_disabled(
|
||||
L10n::l("sample_menus_item_disabled"),
|
||||
|_| "#",
|
||||
.with_item(dropdown::Item::link_blank(
|
||||
L10n::t("menus_dev_forum", &LOC),
|
||||
|_| "https://forum.example.dev".into(),
|
||||
))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::header(L10n::t("menus_sdk_header", &LOC)))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_sdk_rust", &LOC),
|
||||
|cx| cx.route("/dev/sdks/rust"),
|
||||
))
|
||||
.with_item(dropdown::Item::link(L10n::t("menus_sdk_js", &LOC), |cx| {
|
||||
cx.route("/dev/sdks/js")
|
||||
}))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_sdk_python", &LOC),
|
||||
|cx| cx.route("/dev/sdks/python"),
|
||||
))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::header(L10n::t("menus_plugin_header", &LOC)))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_plugin_auth", &LOC),
|
||||
|cx| cx.route("/dev/sdks/rust/plugins/auth"),
|
||||
))
|
||||
.with_item(dropdown::Item::link(
|
||||
L10n::t("menus_plugin_cache", &LOC),
|
||||
|cx| cx.route("/dev/sdks/rust/plugins/cache"),
|
||||
))
|
||||
.with_item(dropdown::Item::divider())
|
||||
.with_item(dropdown::Item::label(L10n::t("menus_item_label", &LOC)))
|
||||
.with_item(dropdown::Item::link_disabled(
|
||||
L10n::t("menus_item_disabled", &LOC),
|
||||
|cx| cx.route("#"),
|
||||
)),
|
||||
))
|
||||
.add_item(nav::Item::link_disabled(
|
||||
L10n::l("sample_menus_item_disabled"),
|
||||
|_| "#",
|
||||
.with_item(nav::Item::link_disabled(
|
||||
L10n::t("menus_item_disabled", &LOC),
|
||||
|cx| cx.route("#"),
|
||||
)),
|
||||
))
|
||||
.add_item(navbar::Item::nav(
|
||||
.with_item(navbar::Item::nav(
|
||||
Nav::new()
|
||||
.with_classes(
|
||||
ClassesOp::Add,
|
||||
classes::Margin::with(Side::Start, ScaleSize::Auto).to_class(),
|
||||
)
|
||||
.add_item(nav::Item::link(
|
||||
L10n::l("sample_menus_item_sign_up"),
|
||||
|_| "/auth/sign-up",
|
||||
))
|
||||
.add_item(nav::Item::link(L10n::l("sample_menus_item_login"), |_| {
|
||||
"/auth/login"
|
||||
.with_item(nav::Item::link(L10n::t("menus_item_sign_up", &LOC), |cx| {
|
||||
cx.route("/auth/sign-up")
|
||||
}))
|
||||
.with_item(nav::Item::link(L10n::t("menus_item_login", &LOC), |cx| {
|
||||
cx.route("/auth/login")
|
||||
})),
|
||||
));
|
||||
|
||||
InRegion::Named("header").add(Child::with(
|
||||
InRegion::Global(&DefaultRegion::Header).add(
|
||||
Container::new()
|
||||
.with_width(container::Width::FluidMax(UnitValue::RelRem(75.0)))
|
||||
.add_child(navbar_menu),
|
||||
));
|
||||
.with_child(navbar_menu),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@
|
|||
<br>
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-aliner = "..."
|
||||
pagetop-aliner = { ... }
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
|
|
@ -59,7 +59,7 @@ Y **selecciona el tema en la configuración** de la aplicación:
|
|||
theme = "Aliner"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
|
@ -80,14 +80,14 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
```
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-aliner = "..."
|
||||
pagetop-aliner = { ... }
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
|
|
@ -60,7 +60,7 @@ Y **selecciona el tema en la configuración** de la aplicación:
|
|||
theme = "Aliner"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
|
@ -69,10 +69,10 @@ use pagetop_aliner::Aliner;
|
|||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Aliner)
|
||||
.add_child(
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
.with_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
|
|
@ -104,12 +104,25 @@ impl Extension for Aliner {
|
|||
}
|
||||
|
||||
impl Theme for Aliner {
|
||||
fn after_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_param("include_basic_assets", true)
|
||||
.alter_assets(ContextOp::AddStyleSheet(
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/normalize.css")
|
||||
.with_version("8.0.1")
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/css/basic.css")
|
||||
.with_version(PAGETOP_VERSION)
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/aliner/css/styles.css")
|
||||
.with_version(env!("CARGO_PKG_VERSION"))
|
||||
.with_weight(-90),
|
||||
));
|
||||
.with_weight(-99),
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,14 @@ h1, h2, h3, h4,h5, h6, p {
|
|||
}
|
||||
|
||||
*::before, *::after {
|
||||
background: #faa;
|
||||
border-radius: 3px;
|
||||
font: normal normal 400 10px/1.2 monospace;
|
||||
vertical-align: middle;
|
||||
padding: 1px 3px;
|
||||
margin: 0 3px;
|
||||
background: #faa;
|
||||
color: #fff;
|
||||
-webkit-text-fill-color: currentColor;
|
||||
}
|
||||
*::before {
|
||||
content: "(";
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@
|
|||
<br>
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-bootsier = "..."
|
||||
pagetop-bootsier = { ... }
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
|
|
@ -59,7 +59,7 @@ Y **selecciona el tema en la configuración** de la aplicación:
|
|||
theme = "Bootsier"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
|
@ -80,14 +80,14 @@ async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
|||
```
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Igual que con otras extensiones, **añade la dependencia** a tu `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
pagetop-bootsier = "..."
|
||||
pagetop-bootsier = { ... }
|
||||
```
|
||||
|
||||
**Declara la extensión** en tu aplicación (o extensión que la requiera). Recuerda que el orden en
|
||||
|
|
@ -60,7 +60,7 @@ Y **selecciona el tema en la configuración** de la aplicación:
|
|||
theme = "Bootsier"
|
||||
```
|
||||
|
||||
…o **fuerza el tema por código** en una página concreta:
|
||||
o **fuerza el tema por código** en una página concreta:
|
||||
|
||||
```rust,no_run
|
||||
use pagetop::prelude::*;
|
||||
|
|
@ -69,10 +69,10 @@ use pagetop_bootsier::Bootsier;
|
|||
async fn homepage(request: HttpRequest) -> ResultPage<Markup, ErrorPage> {
|
||||
Page::new(request)
|
||||
.with_theme(&Bootsier)
|
||||
.add_child(
|
||||
.with_child(
|
||||
Block::new()
|
||||
.with_title(L10n::l("sample_title"))
|
||||
.add_child(Html::with(|cx| html! {
|
||||
.with_child(Html::with(|cx| html! {
|
||||
p { (L10n::l("sample_content").using(cx)) }
|
||||
})),
|
||||
)
|
||||
|
|
@ -102,6 +102,34 @@ pub mod prelude {
|
|||
pub use crate::theme::*;
|
||||
}
|
||||
|
||||
/// Plantillas que Bootsier añade.
|
||||
#[derive(AutoDefault)]
|
||||
pub enum BootsierTemplate {
|
||||
/// Plantilla predeterminada de Bootsier.
|
||||
#[default]
|
||||
Standard,
|
||||
}
|
||||
|
||||
impl Template for BootsierTemplate {
|
||||
fn render(&'static self, cx: &mut Context) -> Markup {
|
||||
match self {
|
||||
Self::Standard => theme::Container::new()
|
||||
.with_classes(ClassesOp::Add, "container-wrapper")
|
||||
.with_width(theme::container::Width::FluidMax(
|
||||
config::SETTINGS.bootsier.max_width,
|
||||
))
|
||||
.with_child(Html::with(|cx| {
|
||||
html! {
|
||||
(DefaultRegion::Header.render(cx))
|
||||
(DefaultRegion::Content.render(cx))
|
||||
(DefaultRegion::Footer.render(cx))
|
||||
}
|
||||
})),
|
||||
}
|
||||
.render(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementa el tema.
|
||||
pub struct Bootsier;
|
||||
|
||||
|
|
@ -117,16 +145,25 @@ impl Extension for Bootsier {
|
|||
}
|
||||
|
||||
impl Theme for Bootsier {
|
||||
fn after_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(ContextOp::AddStyleSheet(
|
||||
#[inline]
|
||||
fn default_template(&self) -> TemplateRef {
|
||||
&BootsierTemplate::Standard
|
||||
}
|
||||
|
||||
fn before_render_page_body(&self, page: &mut Page) {
|
||||
page.alter_assets(AssetsOp::AddStyleSheet(
|
||||
StyleSheet::from("/bootsier/bs/bootstrap.min.css")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
))
|
||||
.alter_assets(ContextOp::AddJavaScript(
|
||||
.alter_assets(AssetsOp::AddJavaScript(
|
||||
JavaScript::defer("/bootsier/js/bootstrap.bundle.min.js")
|
||||
.with_version(BOOTSTRAP_VERSION)
|
||||
.with_weight(-90),
|
||||
));
|
||||
))
|
||||
.alter_child_in(
|
||||
&DefaultRegion::Footer,
|
||||
ChildOp::AddIfEmpty(PoweredBy::new().into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
e404-description = Oops! Page Not Found
|
||||
e404-message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable.
|
||||
e500-description = Oops! Unexpected Error
|
||||
e500-message = We're having an issue. Please report this error to an administrator.
|
||||
back-homepage = Back to homepage
|
||||
e404_description = Oops! Page Not Found
|
||||
e404_message = The page you are looking for may have been removed, had its name changed, or is temporarily unavailable.
|
||||
e500_description = Oops! Unexpected Error
|
||||
e500_message = We're having an issue. Please report this error to an administrator.
|
||||
back_homepage = Back to homepage
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Toggle Dropdown
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Close
|
||||
# form::Input
|
||||
input_required = This field is required
|
||||
|
||||
# Navbar
|
||||
toggle = Toggle navigation
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Close
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
header = Header
|
||||
nav_branding = Navigation branding region
|
||||
nav_main = Main navigation region
|
||||
nav_additional = Additional navigation region (eg search form, social icons, etc)
|
||||
breadcrumb = Breadcrumb
|
||||
content = Main content
|
||||
sidebar_first = Sidebar first
|
||||
sidebar_second = Sidebar second
|
||||
footer = Footer
|
||||
region_header = Header
|
||||
region_nav_branding = Navigation branding region
|
||||
region_nav_main = Main navigation region
|
||||
region_nav_additional = Additional navigation region (eg search form, social icons, etc)
|
||||
region_breadcrumb = Breadcrumb
|
||||
region_content = Main content
|
||||
region_sidebar_first = Sidebar first
|
||||
region_sidebar_second = Sidebar second
|
||||
region_footer = Footer
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
e404-description = ¡Vaya! Página No Encontrada
|
||||
e404-message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente.
|
||||
e500-description = ¡Vaya! Error Inesperado
|
||||
e500-message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador.
|
||||
back-homepage = Volver al inicio
|
||||
e404_description = ¡Vaya! Página No Encontrada
|
||||
e404_message = La página que está buscando puede haber sido eliminada, cambiada de nombre o no está disponible temporalmente.
|
||||
e500_description = ¡Vaya! Error Inesperado
|
||||
e500_message = Está ocurriendo una incidencia. Por favor, informe de este error a un administrador.
|
||||
back_homepage = Volver al inicio
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# Dropdown
|
||||
dropdown_toggle = Mostrar/ocultar menú
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Cerrar
|
||||
# form::Input
|
||||
input_required = Este campo es obligatorio
|
||||
|
||||
# Navbar
|
||||
toggle = Mostrar/ocultar navegación
|
||||
|
||||
# Offcanvas
|
||||
offcanvas_close = Cerrar
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
header = Cabecera
|
||||
nav_branding = Navegación y marca
|
||||
nav_main = Navegación principal
|
||||
nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.)
|
||||
breadcrumb = Ruta de posicionamiento
|
||||
content = Contenido principal
|
||||
sidebar_first = Barra lateral primera
|
||||
sidebar_second = Barra lateral segunda
|
||||
footer = Pie
|
||||
region_header = Cabecera
|
||||
region_nav_branding = Navegación y marca
|
||||
region_nav_main = Navegación principal
|
||||
region_nav_additional = Navegación adicional (p.e. formulario de búsqueda, iconos sociales, etc.)
|
||||
region_breadcrumb = Ruta de posicionamiento
|
||||
region_content = Contenido principal
|
||||
region_sidebar_first = Barra lateral primera
|
||||
region_sidebar_second = Barra lateral segunda
|
||||
region_footer = Pie
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ pub use aux::*;
|
|||
|
||||
pub mod classes;
|
||||
|
||||
// Button.
|
||||
mod button;
|
||||
pub use button::Button;
|
||||
|
||||
// Container.
|
||||
pub mod container;
|
||||
#[doc(inline)]
|
||||
|
|
@ -19,6 +23,11 @@ pub mod dropdown;
|
|||
#[doc(inline)]
|
||||
pub use dropdown::Dropdown;
|
||||
|
||||
// Form.
|
||||
pub mod form;
|
||||
#[doc(inline)]
|
||||
pub use form::Form;
|
||||
|
||||
// Image.
|
||||
pub mod image;
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -17,4 +17,4 @@ mod rounded;
|
|||
pub use rounded::RoundedRadius;
|
||||
|
||||
mod button;
|
||||
pub use button::{ButtonColor, ButtonSize};
|
||||
pub use button::{ButtonAction, ButtonColor, ButtonSize};
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ use pagetop::prelude::*;
|
|||
|
||||
use crate::theme::aux::Color;
|
||||
|
||||
/// Colores `border-*` para los bordes ([`classes::Border`](crate::theme::classes::Border)).
|
||||
/// Esquema de color para los bordes ([`classes::Border`](crate::theme::classes::Border)).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum BorderColor {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
Default,
|
||||
/// Genera internamente clases `border-{color}`.
|
||||
/// Genera la clase `border-{color}`.
|
||||
Theme(Color),
|
||||
/// Genera internamente clases `border-{color}-subtle` (un tono suavizado del color).
|
||||
/// Genera la clase `border-{color}-subtle` (un tono suavizado del color).
|
||||
Subtle(Color),
|
||||
/// Color negro.
|
||||
Black,
|
||||
|
|
@ -22,7 +22,7 @@ impl BorderColor {
|
|||
const BORDER: &str = "border";
|
||||
const BORDER_PREFIX: &str = "border-";
|
||||
|
||||
// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
|
||||
/// Devuelve el sufijo de la clase `border-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -35,7 +35,7 @@ impl BorderColor {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase `border-*` a la cadena de clases.
|
||||
/// Añade la clase `border-*` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -64,7 +64,6 @@ impl BorderColor {
|
|||
/// assert_eq!(BorderColor::Black.to_class(), "border-black");
|
||||
/// assert_eq!(BorderColor::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = match self {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub enum BreakPoint {
|
|||
}
|
||||
|
||||
impl BreakPoint {
|
||||
// Devuelve la identificación del punto de ruptura.
|
||||
/// Devuelve la identificación del punto de ruptura.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
|
|
@ -33,11 +33,11 @@ impl BreakPoint {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade el punto de ruptura con un prefijo y un sufijo (opcional) separados por un guion `-` a
|
||||
// la cadena de clases.
|
||||
//
|
||||
// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
||||
// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
||||
/// Añade el punto de ruptura con un prefijo y un sufijo (opcional) separados por un guion `-` a
|
||||
/// la cadena de clases.
|
||||
///
|
||||
/// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
||||
/// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str, suffix: &str) {
|
||||
if prefix.is_empty() {
|
||||
|
|
@ -60,30 +60,30 @@ impl BreakPoint {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve la clase para el punto de ruptura, con un prefijo y un sufijo opcional, separados
|
||||
// por un guion `-`.
|
||||
//
|
||||
// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
||||
// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
||||
// - Si `prefix` está vacío devuelve `""`.
|
||||
//
|
||||
// # Ejemplos
|
||||
//
|
||||
// ```rust
|
||||
// # use pagetop_bootsier::prelude::*;
|
||||
// let bp = BreakPoint::MD;
|
||||
// assert_eq!(bp.class_with("col", ""), "col-md");
|
||||
// assert_eq!(bp.class_with("col", "6"), "col-md-6");
|
||||
//
|
||||
// let bp = BreakPoint::None;
|
||||
// assert_eq!(bp.class_with("offcanvas", ""), "offcanvas");
|
||||
// assert_eq!(bp.class_with("col", "12"), "col-12");
|
||||
//
|
||||
// let bp = BreakPoint::LG;
|
||||
// assert_eq!(bp.class_with("", "3"), "");
|
||||
// ```
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, prefix: &str, suffix: &str) -> String {
|
||||
/// Devuelve la clase para el punto de ruptura, con un prefijo y un sufijo opcional, separados
|
||||
/// por un guion `-`.
|
||||
///
|
||||
/// - Para `None` - `prefix` o `prefix-suffix` (si `suffix` no está vacío).
|
||||
/// - Para `SM..XXL` - `prefix-{breakpoint}` o `prefix-{breakpoint}-{suffix}`.
|
||||
/// - Si `prefix` está vacío devuelve `""`.
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let bp = BreakPoint::MD;
|
||||
/// assert_eq!(bp.class_with("col", ""), "col-md");
|
||||
/// assert_eq!(bp.class_with("col", "6"), "col-md-6");
|
||||
///
|
||||
/// let bp = BreakPoint::None;
|
||||
/// assert_eq!(bp.class_with("offcanvas", ""), "offcanvas");
|
||||
/// assert_eq!(bp.class_with("col", "12"), "col-12");
|
||||
///
|
||||
/// let bp = BreakPoint::LG;
|
||||
/// assert_eq!(bp.class_with("", "3"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, prefix: &str, suffix: &str) -> String {
|
||||
if prefix.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,42 @@ use pagetop::prelude::*;
|
|||
|
||||
use crate::theme::aux::Color;
|
||||
|
||||
// **< ButtonAction >*********************************************************************************
|
||||
|
||||
/// Comportamiento de un [`Button`](crate::theme::Button) al activarse.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonAction {
|
||||
/// Envía un formulario al servidor. Es el **tipo por defecto**.
|
||||
#[default]
|
||||
Submit,
|
||||
/// Restablece todos los campos de un formulario a sus valores iniciales.
|
||||
Reset,
|
||||
/// Botón de propósito general, sin efecto predeterminado. Su comportamiento podría definirse
|
||||
/// mediante JavaScript.
|
||||
Plain,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ButtonAction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
ButtonAction::Submit => "submit",
|
||||
ButtonAction::Reset => "reset",
|
||||
ButtonAction::Plain => "button",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// **< ButtonColor >********************************************************************************
|
||||
|
||||
/// Variantes de color `btn-*` para botones.
|
||||
/// Esquema de color para [`Button`](crate::theme::Button).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ButtonColor {
|
||||
/// No define ninguna clase.
|
||||
#[default]
|
||||
Default,
|
||||
/// Genera internamente clases `btn-{color}` (botón relleno).
|
||||
/// Genera la clase `btn-{color}` (botón sólido).
|
||||
Background(Color),
|
||||
/// Genera `btn-outline-{color}` (fondo transparente y contorno con borde).
|
||||
/// Genera la clase `btn-outline-{color}` (fondo transparente con contorno coloreado).
|
||||
Outline(Color),
|
||||
/// Aplica estilo de los enlaces (`btn-link`), sin caja ni fondo, heredando el color de texto.
|
||||
Link,
|
||||
|
|
@ -23,7 +48,7 @@ impl ButtonColor {
|
|||
const BTN_OUTLINE_PREFIX: &str = "btn-outline-";
|
||||
const BTN_LINK: &str = "btn-link";
|
||||
|
||||
// Añade la clase `btn-*` a la cadena de clases.
|
||||
/// Añade la clase `btn-*` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Self::Default = self {
|
||||
|
|
@ -33,7 +58,6 @@ impl ButtonColor {
|
|||
classes.push(' ');
|
||||
}
|
||||
match self {
|
||||
Self::Default => unreachable!(),
|
||||
Self::Background(c) => {
|
||||
classes.push_str(Self::BTN_PREFIX);
|
||||
classes.push_str(c.as_str());
|
||||
|
|
@ -42,9 +66,8 @@ impl ButtonColor {
|
|||
classes.push_str(Self::BTN_OUTLINE_PREFIX);
|
||||
classes.push_str(c.as_str());
|
||||
}
|
||||
Self::Link => {
|
||||
classes.push_str(Self::BTN_LINK);
|
||||
}
|
||||
Self::Link => classes.push_str(Self::BTN_LINK),
|
||||
Self::Default => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,27 +88,11 @@ impl ButtonColor {
|
|||
/// assert_eq!(ButtonColor::Link.to_class(), "btn-link");
|
||||
/// assert_eq!(ButtonColor::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
match self {
|
||||
Self::Default => String::new(),
|
||||
Self::Background(c) => {
|
||||
let color = c.as_str();
|
||||
let mut class = String::with_capacity(Self::BTN_PREFIX.len() + color.len());
|
||||
class.push_str(Self::BTN_PREFIX);
|
||||
class.push_str(color);
|
||||
let mut class = String::new();
|
||||
self.push_class(&mut class);
|
||||
class
|
||||
}
|
||||
Self::Outline(c) => {
|
||||
let color = c.as_str();
|
||||
let mut class = String::with_capacity(Self::BTN_OUTLINE_PREFIX.len() + color.len());
|
||||
class.push_str(Self::BTN_OUTLINE_PREFIX);
|
||||
class.push_str(color);
|
||||
class
|
||||
}
|
||||
Self::Link => Self::BTN_LINK.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< ButtonSize >*********************************************************************************
|
||||
|
|
@ -98,7 +105,7 @@ pub enum ButtonSize {
|
|||
Default,
|
||||
/// Botón compacto.
|
||||
Small,
|
||||
/// Botón destacado/grande.
|
||||
/// Botón grande.
|
||||
Large,
|
||||
}
|
||||
|
||||
|
|
@ -106,20 +113,18 @@ impl ButtonSize {
|
|||
const BTN_SM: &str = "btn-sm";
|
||||
const BTN_LG: &str = "btn-lg";
|
||||
|
||||
// Añade la clase de tamaño `btn-sm` o `btn-lg` a la cadena de clases.
|
||||
/// Añade la clase de tamaño `btn-sm` o `btn-lg` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Self::Default = self {
|
||||
return;
|
||||
}
|
||||
let class = match self {
|
||||
Self::Default => return,
|
||||
Self::Small => Self::BTN_SM,
|
||||
Self::Large => Self::BTN_LG,
|
||||
};
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
match self {
|
||||
Self::Default => unreachable!(),
|
||||
Self::Small => classes.push_str(Self::BTN_SM),
|
||||
Self::Large => classes.push_str(Self::BTN_LG),
|
||||
}
|
||||
classes.push_str(class);
|
||||
}
|
||||
|
||||
/// Devuelve la clase `btn-sm` o `btn-lg` correspondiente al tamaño del botón.
|
||||
|
|
@ -132,12 +137,9 @@ impl ButtonSize {
|
|||
/// assert_eq!(ButtonSize::Large.to_class(), "btn-lg");
|
||||
/// assert_eq!(ButtonSize::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
match self {
|
||||
Self::Default => String::new(),
|
||||
Self::Small => Self::BTN_SM.to_string(),
|
||||
Self::Large => Self::BTN_LG.to_string(),
|
||||
}
|
||||
let mut class = String::new();
|
||||
self.push_class(&mut class);
|
||||
class
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ use pagetop::prelude::*;
|
|||
/// Paleta de colores temáticos.
|
||||
///
|
||||
/// Equivalen a los nombres estándar definidos por Bootstrap (`primary`, `secondary`, `success`,
|
||||
/// etc.). Este tipo enumerado sirve de base para componer las clases de color para fondo
|
||||
/// ([`classes::Background`](crate::theme::classes::Background)), bordes
|
||||
/// ([`classes::Border`](crate::theme::classes::Border)) y texto
|
||||
/// etc.). Este tipo enumerado sirve de referencia para componer las clases de color para el fondo
|
||||
/// ([`classes::Background`](crate::theme::classes::Background)), los bordes
|
||||
/// ([`classes::Border`](crate::theme::classes::Border)) o para el texto
|
||||
/// ([`classes::Text`](crate::theme::classes::Text)).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Color {
|
||||
|
|
@ -23,7 +23,7 @@ pub enum Color {
|
|||
}
|
||||
|
||||
impl Color {
|
||||
// Devuelve el nombre del color.
|
||||
/// Devuelve el nombre del color.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
|
|
@ -39,15 +39,6 @@ impl Color {
|
|||
}
|
||||
}
|
||||
|
||||
/* Añade el nombre del color a la cadena de clases (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if !classes.is_empty() {
|
||||
classes.push(' ');
|
||||
}
|
||||
classes.push_str(self.as_str());
|
||||
} */
|
||||
|
||||
/// Devuelve la clase correspondiente al color.
|
||||
///
|
||||
/// # Ejemplos
|
||||
|
|
@ -94,7 +85,7 @@ impl Opacity {
|
|||
const OPACITY: &str = "opacity";
|
||||
const OPACITY_PREFIX: &str = "-opacity";
|
||||
|
||||
// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
|
||||
/// Devuelve el sufijo para `*opacity-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -109,8 +100,8 @@ impl Opacity {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la opacidad a la cadena de clases usando el prefijo dado (`bg`, `border`, `text`, o
|
||||
// vacío para `opacity-*`).
|
||||
/// Añade la opacidad a la cadena de clases usando el prefijo dado (`bg`, `border`, `text`, o
|
||||
/// vacío para `opacity-*`).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -127,20 +118,20 @@ impl Opacity {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve la clase de opacidad con el prefijo dado (`bg`, `border`, `text`, o vacío para
|
||||
// `opacity-*`).
|
||||
//
|
||||
// # Ejemplos
|
||||
//
|
||||
// ```rust
|
||||
// # use pagetop_bootsier::prelude::*;
|
||||
// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
|
||||
// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
|
||||
// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
|
||||
// assert_eq!(Opacity::Default.class_with("bg"), "");
|
||||
// ```
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
||||
/// Devuelve la clase de opacidad con el prefijo dado (`bg`, `border`, `text`, o vacío para
|
||||
/// `opacity-*`).
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(Opacity::Opaque.class_with(""), "opacity-100");
|
||||
/// assert_eq!(Opacity::Half.class_with("bg"), "bg-opacity-50");
|
||||
/// assert_eq!(Opacity::SemiTransparent.class_with("text"), "text-opacity-25");
|
||||
/// assert_eq!(Opacity::Default.class_with("bg"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, prefix: &str) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = if prefix.is_empty() {
|
||||
Self::OPACITY.len()
|
||||
|
|
@ -178,7 +169,7 @@ impl Opacity {
|
|||
|
||||
// **< ColorBg >************************************************************************************
|
||||
|
||||
/// Colores `bg-*` para el fondo.
|
||||
/// Esquema de color para el fondo.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ColorBg {
|
||||
/// No define ninguna clase.
|
||||
|
|
@ -190,9 +181,9 @@ pub enum ColorBg {
|
|||
BodySecondary,
|
||||
/// Fondo predefinido del tema (`bg-body-tertiary`).
|
||||
BodyTertiary,
|
||||
/// Genera internamente clases `bg-{color}` (p. ej., `bg-primary`).
|
||||
/// Genera la clase `bg-{color}` (p. ej., `bg-primary`).
|
||||
Theme(Color),
|
||||
/// Genera internamente clases `bg-{color}-subtle` (un tono suavizado del color).
|
||||
/// Genera la clase `bg-{color}-subtle` (un tono suavizado del color).
|
||||
Subtle(Color),
|
||||
/// Color negro.
|
||||
Black,
|
||||
|
|
@ -206,7 +197,7 @@ impl ColorBg {
|
|||
const BG: &str = "bg";
|
||||
const BG_PREFIX: &str = "bg-";
|
||||
|
||||
// Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase.
|
||||
/// Devuelve el sufijo de la clase `bg-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -223,7 +214,7 @@ impl ColorBg {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase de fondo `bg-*` a la cadena de clases.
|
||||
/// Añade la clase de fondo `bg-*` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -253,31 +244,16 @@ impl ColorBg {
|
|||
/// assert_eq!(ColorBg::Transparent.to_class(), "bg-transparent");
|
||||
/// assert_eq!(ColorBg::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = match self {
|
||||
Self::Theme(c) | Self::Subtle(c) => Self::BG_PREFIX.len() + c.as_str().len(),
|
||||
_ => Self::BG.len(),
|
||||
};
|
||||
let mut class = String::with_capacity(base_len + suffix.len());
|
||||
match self {
|
||||
Self::Theme(c) | Self::Subtle(c) => {
|
||||
class.push_str(Self::BG_PREFIX);
|
||||
class.push_str(c.as_str());
|
||||
}
|
||||
_ => class.push_str(Self::BG),
|
||||
}
|
||||
class.push_str(suffix);
|
||||
return class;
|
||||
}
|
||||
String::new()
|
||||
let mut class = String::new();
|
||||
self.push_class(&mut class);
|
||||
class
|
||||
}
|
||||
}
|
||||
|
||||
// **< ColorText >**********************************************************************************
|
||||
|
||||
/// Colores `text-*` para el texto.
|
||||
/// Esquema de color para el texto.
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum ColorText {
|
||||
/// No define ninguna clase.
|
||||
|
|
@ -291,9 +267,9 @@ pub enum ColorText {
|
|||
BodySecondary,
|
||||
/// Color predefinido del tema (`text-body-tertiary`).
|
||||
BodyTertiary,
|
||||
/// Genera internamente clases `text-{color}`.
|
||||
/// Genera la clase `text-{color}`.
|
||||
Theme(Color),
|
||||
/// Genera internamente clases `text-{color}-emphasis` (mayor contraste acorde al tema).
|
||||
/// Genera la clase `text-{color}-emphasis` (mayor contraste acorde al tema).
|
||||
Emphasis(Color),
|
||||
/// Color negro.
|
||||
Black,
|
||||
|
|
@ -305,7 +281,7 @@ impl ColorText {
|
|||
const TEXT: &str = "text";
|
||||
const TEXT_PREFIX: &str = "text-";
|
||||
|
||||
// Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase.
|
||||
/// Devuelve el sufijo de la clase `text-*`, o `None` si no define ninguna clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -322,7 +298,7 @@ impl ColorText {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase de texto `text-*` a la cadena de clases.
|
||||
/// Añade la clase de texto `text-*` a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -352,24 +328,9 @@ impl ColorText {
|
|||
/// assert_eq!(ColorText::Black.to_class(), "text-black");
|
||||
/// assert_eq!(ColorText::Default.to_class(), "");
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = match self {
|
||||
Self::Theme(c) | Self::Emphasis(c) => Self::TEXT_PREFIX.len() + c.as_str().len(),
|
||||
_ => Self::TEXT.len(),
|
||||
};
|
||||
let mut class = String::with_capacity(base_len + suffix.len());
|
||||
match self {
|
||||
Self::Theme(c) | Self::Emphasis(c) => {
|
||||
class.push_str(Self::TEXT_PREFIX);
|
||||
class.push_str(c.as_str());
|
||||
}
|
||||
_ => class.push_str(Self::TEXT),
|
||||
}
|
||||
class.push_str(suffix);
|
||||
return class;
|
||||
}
|
||||
String::new()
|
||||
let mut class = String::new();
|
||||
self.push_class(&mut class);
|
||||
class
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ pub enum ScaleSize {
|
|||
}
|
||||
|
||||
impl ScaleSize {
|
||||
// Devuelve el sufijo para el tamaño (`"-0"`, `"-1"`, etc.), o `None` si no define ninguna
|
||||
// clase, o `""` para el tamaño automático.
|
||||
/// Devuelve el sufijo para el tamaño (`"-0"`, `"-1"`, etc.), o `None` si no define ninguna
|
||||
/// clase, o `""` para el tamaño automático.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -42,7 +42,7 @@ impl ScaleSize {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade el tamaño a la cadena de clases usando el prefijo dado.
|
||||
/// Añade el tamaño a la cadena de clases usando el prefijo dado.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
||||
if !prefix.is_empty() {
|
||||
|
|
@ -57,18 +57,18 @@ impl ScaleSize {
|
|||
}
|
||||
|
||||
/* Devuelve la clase del tamaño para el prefijo, o una cadena vacía si no aplica (reservado).
|
||||
//
|
||||
// # Ejemplo
|
||||
//
|
||||
// ```rust
|
||||
// # use pagetop_bootsier::prelude::*;
|
||||
// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
|
||||
// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
|
||||
// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
|
||||
// assert_eq!(ScaleSize::None.class_with("border"), "");
|
||||
// ```
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(ScaleSize::Auto.class_with("border"), "border");
|
||||
/// assert_eq!(ScaleSize::Zero.class_with("m"), "m-0");
|
||||
/// assert_eq!(ScaleSize::Three.class_with("p"), "p-3");
|
||||
/// assert_eq!(ScaleSize::None.class_with("border"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, prefix: &str) -> String {
|
||||
if !prefix.is_empty() {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let mut class = String::with_capacity(prefix.len() + suffix.len());
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ pub enum RoundedRadius {
|
|||
impl RoundedRadius {
|
||||
const ROUNDED: &str = "rounded";
|
||||
|
||||
// Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el
|
||||
// redondeo por defecto.
|
||||
/// Devuelve el sufijo para `*rounded-*`, o `None` si no define ninguna clase, o `""` para el
|
||||
/// redondeo por defecto.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(self) -> Option<&'static str> {
|
||||
|
|
@ -48,8 +48,8 @@ impl RoundedRadius {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`,
|
||||
// `rounded-bottom-start`, o vacío para `rounded-*`).
|
||||
/// Añade el redondeo de esquinas a la cadena de clases usando el prefijo dado (`rounded-top`,
|
||||
/// `rounded-bottom-start`, o vacío para `rounded-*`).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, prefix: &str) {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
|
|
@ -65,21 +65,21 @@ impl RoundedRadius {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve la clase para el redondeo de esquinas con el prefijo dado (`rounded-top`,
|
||||
// `rounded-bottom-start`, o vacío para `rounded-*`).
|
||||
//
|
||||
// # Ejemplos
|
||||
//
|
||||
// ```rust
|
||||
// # use pagetop_bootsier::prelude::*;
|
||||
// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
|
||||
// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
|
||||
// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
|
||||
// assert_eq!(RoundedRadius::Circle.class_with(""), "rounded-circle");
|
||||
// assert_eq!(RoundedRadius::None.class_with("rounded-bottom-start"), "");
|
||||
// ```
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, prefix: &str) -> String {
|
||||
/// Devuelve la clase para el redondeo de esquinas con el prefijo dado (`rounded-top`,
|
||||
/// `rounded-bottom-start`, o vacío para `rounded-*`).
|
||||
///
|
||||
/// # Ejemplos
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// assert_eq!(RoundedRadius::Scale2.class_with(""), "rounded-2");
|
||||
/// assert_eq!(RoundedRadius::Zero.class_with("rounded-top"), "rounded-top-0");
|
||||
/// assert_eq!(RoundedRadius::Scale3.class_with("rounded-top-end"), "rounded-top-end-3");
|
||||
/// assert_eq!(RoundedRadius::Circle.class_with(""), "rounded-circle");
|
||||
/// assert_eq!(RoundedRadius::None.class_with("rounded-bottom-start"), "");
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, prefix: &str) -> String {
|
||||
if let Some(suffix) = self.suffix() {
|
||||
let base_len = if prefix.is_empty() {
|
||||
Self::ROUNDED.len()
|
||||
|
|
|
|||
213
extensions/pagetop-bootsier/src/theme/button.rs
Normal file
213
extensions/pagetop-bootsier/src/theme/button.rs
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::{ButtonAction, ButtonColor, ButtonSize};
|
||||
|
||||
/// Componente para crear un **botón**.
|
||||
///
|
||||
/// Renderiza un botón con soporte para las variantes disponibles en [`ButtonAction`] (`submit`,
|
||||
/// `reset` y botón genérico) y con la variedad de estilos del tema a través de [`ButtonColor`] y
|
||||
/// [`ButtonSize`].
|
||||
///
|
||||
/// El comportamiento del botón se establece al crearlo:
|
||||
///
|
||||
/// - [`Button::submit()`]: botón de envío (por defecto).
|
||||
/// - [`Button::reset()`]: botón de restablecimiento de valores.
|
||||
/// - [`Button::plain()`]: botón genérico sin comportamiento predeterminado.
|
||||
///
|
||||
/// El botón puede usarse dentro o fuera de un formulario.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let save = Button::submit(L10n::n("Save"))
|
||||
/// .with_color(ButtonColor::Background(Color::Primary));
|
||||
///
|
||||
/// let cancel = Button::plain(L10n::n("Cancel"))
|
||||
/// .with_color(ButtonColor::Outline(Color::Secondary));
|
||||
///
|
||||
/// let clear = Button::reset(L10n::n("Clear"))
|
||||
/// .with_size(ButtonSize::Small);
|
||||
/// ```
|
||||
///
|
||||
/// Cuando el botón activa el envío, el navegador incluye el par `name=value` en los datos del
|
||||
/// formulario **sólo si** tiene el atributo `name` definido. Es la forma habitual de identificar
|
||||
/// cuál de los botones de envío fue pulsado. En el servidor se deserializa como `Option<String>`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// #[serde(default)]
|
||||
/// action: Option<String>, // p. ej., "save" o "delete"; `None` si el botón no tenía `name`.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Button {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del botón.
|
||||
classes: Classes,
|
||||
/// Devuelve el comportamiento del botón al activarse.
|
||||
kind: ButtonAction,
|
||||
/// Devuelve el esquema de color del botón.
|
||||
color: ButtonColor,
|
||||
/// Devuelve el tamaño visual del botón.
|
||||
size: ButtonSize,
|
||||
/// Devuelve el nombre del botón.
|
||||
name: AttrName,
|
||||
/// Devuelve el valor del botón.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta del botón.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si el botón recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el botón está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Component for Button {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
let mut classes = "btn".to_string();
|
||||
(*self.color()).push_class(&mut classes);
|
||||
(*self.size()).push_class(&mut classes);
|
||||
self.alter_classes(ClassesOp::Prepend, classes);
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {
|
||||
button
|
||||
id=[self.id()]
|
||||
type=(self.kind())
|
||||
class=[self.classes().get()]
|
||||
name=[self.name().get()]
|
||||
value=[self.value().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
disabled[*self.disabled()]
|
||||
{
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
(label)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Button {
|
||||
/// Crea un botón de **envío** (`type="submit"`).
|
||||
///
|
||||
/// Es la acción predeterminada al pulsar un botón en la mayoría de los formularios: envía los
|
||||
/// datos al servidor.
|
||||
pub fn submit(label: L10n) -> Self {
|
||||
Self {
|
||||
kind: ButtonAction::Submit,
|
||||
label: Attr::some(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un botón de **restablecimiento** (`type="reset"`).
|
||||
///
|
||||
/// Al pulsarlo, devuelve todos los campos del formulario a sus valores iniciales.
|
||||
pub fn reset(label: L10n) -> Self {
|
||||
Self {
|
||||
kind: ButtonAction::Reset,
|
||||
label: Attr::some(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un **botón genérico** (`type="button"`).
|
||||
///
|
||||
/// No tiene un comportamiento predeterminado sobre el formulario. Su comportamiento puede
|
||||
/// definirse mediante JavaScript.
|
||||
pub fn plain(label: L10n) -> Self {
|
||||
Self {
|
||||
kind: ButtonAction::Plain,
|
||||
label: Attr::some(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Button BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al botón.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el esquema de color del botón.
|
||||
///
|
||||
/// Usa [`ButtonColor::Background`] para botones sólidos o [`ButtonColor::Outline`] para
|
||||
/// variantes con contorno.
|
||||
#[builder_fn]
|
||||
pub fn with_color(mut self, color: ButtonColor) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el tamaño visual del botón.
|
||||
#[builder_fn]
|
||||
pub fn with_size(mut self, size: ButtonSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del botón (atributo `name`).
|
||||
///
|
||||
/// Cuando el formulario tiene varios botones de envío, el navegador incluye en el envío el par
|
||||
/// `name=value` sólo del botón que activó el formulario. Permite identificar cuál fue pulsado.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor del botón (atributo `value`).
|
||||
///
|
||||
/// Es el dato que el navegador transmite al servidor junto con el `name` cuando este botón
|
||||
/// activa el envío. Útil para distinguir entre varios botones de envío en un mismo formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
|
||||
self.value.alter_str(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del botón (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el botón recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el botón está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -145,7 +145,6 @@ impl Border {
|
|||
/// `"border border-top-0 border-end-3 border-primary border-opacity-50"`, etc.).
|
||||
///
|
||||
/// Si no se define ningún tamaño, color ni opacidad, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,6 @@ impl Background {
|
|||
/// Devuelve las clases de fondo como cadena (`"bg-primary"`, `"bg-body-secondary bg-opacity-50"`, etc.).
|
||||
///
|
||||
/// Si no se define ni color ni opacidad, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
|
|
@ -189,7 +188,6 @@ impl Text {
|
|||
/// etc.).
|
||||
///
|
||||
/// Si no se define ni color ni opacidad, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ impl Margin {
|
|||
|
||||
// **< Margin HELPERS >*************************************************************************
|
||||
|
||||
// Devuelve el prefijo `m*` según el lado.
|
||||
/// Devuelve el prefijo `m*` según el lado.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn side_prefix(&self) -> &'static str {
|
||||
|
|
@ -63,7 +63,7 @@ impl Margin {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
|
||||
/// Devuelve el sufijo del tamaño (`auto`, `0`..`5`), o `None` si no define clase.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn size_suffix(&self) -> Option<&'static str> {
|
||||
|
|
@ -79,22 +79,9 @@ impl Margin {
|
|||
}
|
||||
}
|
||||
|
||||
/* Añade la clase de **margin** a la cadena de clases (reservado).
|
||||
//
|
||||
// No añade nada si `size` es `ScaleSize::None`.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let Some(size) = self.size_suffix() else {
|
||||
return;
|
||||
};
|
||||
self.breakpoint
|
||||
.push_class(classes, self.side_prefix(), size);
|
||||
} */
|
||||
|
||||
/// Devuelve la clase de **margin** como cadena (`"mt-3"`, `"ms-lg-auto"`, etc.).
|
||||
///
|
||||
/// Si `size` es `ScaleSize::None`, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let Some(size) = self.size_suffix() else {
|
||||
return String::new();
|
||||
|
|
@ -148,7 +135,7 @@ impl Padding {
|
|||
|
||||
// **< Padding HELPERS >************************************************************************
|
||||
|
||||
// Devuelve el prefijo `p*` según el lado.
|
||||
/// Devuelve el prefijo `p*` según el lado.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn prefix(&self) -> &'static str {
|
||||
|
|
@ -163,9 +150,9 @@ impl Padding {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve el sufijo del tamaño (`0`..`5`), o `None` si no define clase.
|
||||
//
|
||||
// Nota: `ScaleSize::Auto` **no aplica** a padding ⇒ devuelve `None`.
|
||||
/// Devuelve el sufijo del tamaño (`0`..`5`), o `None` si no define clase.
|
||||
///
|
||||
/// Nota: `ScaleSize::Auto` **no aplica** a padding ⇒ devuelve `None`.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn suffix(&self) -> Option<&'static str> {
|
||||
|
|
@ -181,21 +168,9 @@ impl Padding {
|
|||
}
|
||||
}
|
||||
|
||||
/* Añade la clase de **padding** a la cadena de clases (reservado).
|
||||
//
|
||||
// No añade nada si `size` es `ScaleSize::None` o `ScaleSize::Auto`.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let Some(size) = self.suffix() else {
|
||||
return;
|
||||
};
|
||||
self.breakpoint.push_class(classes, self.prefix(), size);
|
||||
} */
|
||||
|
||||
// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
|
||||
//
|
||||
// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
|
||||
#[inline]
|
||||
/// Devuelve la clase de **padding** como cadena (`"px-2"`, `"pe-sm-4"`, etc.).
|
||||
///
|
||||
/// Si `size` es `ScaleSize::None` o `ScaleSize::Auto`, devuelve `""`.
|
||||
pub fn to_class(self) -> String {
|
||||
let Some(size) = self.suffix() else {
|
||||
return String::new();
|
||||
|
|
|
|||
|
|
@ -160,7 +160,6 @@ impl Rounded {
|
|||
/// `"rounded-top rounded-bottom-start-4 rounded-bottom-end-circle"`, etc.).
|
||||
///
|
||||
/// Si no se define ningún radio, devuelve `""`.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
|
|
|
|||
|
|
@ -6,79 +6,83 @@ use crate::prelude::*;
|
|||
///
|
||||
/// Envuelve un contenido con la etiqueta HTML indicada por [`container::Kind`]. Sólo se renderiza
|
||||
/// si existen componentes hijos (*children*).
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Container {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
container_kind : container::Kind,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al contenedor.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo semántico del contenedor.
|
||||
container_kind: container::Kind,
|
||||
/// Devuelve el comportamiento para el ancho del contenedor.
|
||||
container_width: container::Width,
|
||||
children : Children,
|
||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Container {
|
||||
fn new() -> Self {
|
||||
Container::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.width().to_class());
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.container_width().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let output = self.children().render(cx);
|
||||
if output.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
let style = match self.width() {
|
||||
let style = match self.container_width() {
|
||||
container::Width::FluidMax(w) if w.is_measurable() => {
|
||||
Some(join!("max-width: ", w.to_string(), ";"))
|
||||
Some(util::join!("max-width: ", w.to_string(), ";"))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match self.container_kind() {
|
||||
container::Kind::Default => PrepareMarkup::With(html! {
|
||||
Ok(match self.container_kind() {
|
||||
container::Kind::Default => html! {
|
||||
div id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Main => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Main => html! {
|
||||
main id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Header => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Header => html! {
|
||||
header id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Footer => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Footer => html! {
|
||||
footer id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Section => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Section => html! {
|
||||
section id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
container::Kind::Article => PrepareMarkup::With(html! {
|
||||
},
|
||||
container::Kind::Article => html! {
|
||||
article id=[self.id()] class=[self.classes().get()] style=[style] {
|
||||
(output)
|
||||
}
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
/// Crea un contenedor de tipo `Main` (`<main>`).
|
||||
pub fn main() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Main,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -86,7 +90,7 @@ impl Container {
|
|||
|
||||
/// Crea un contenedor de tipo `Header` (`<header>`).
|
||||
pub fn header() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Header,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -94,7 +98,7 @@ impl Container {
|
|||
|
||||
/// Crea un contenedor de tipo `Footer` (`<footer>`).
|
||||
pub fn footer() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Footer,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -102,7 +106,7 @@ impl Container {
|
|||
|
||||
/// Crea un contenedor de tipo `Section` (`<section>`).
|
||||
pub fn section() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Section,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -110,7 +114,7 @@ impl Container {
|
|||
|
||||
/// Crea un contenedor de tipo `Article` (`<article>`).
|
||||
pub fn article() -> Self {
|
||||
Container {
|
||||
Self {
|
||||
container_kind: container::Kind::Article,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -121,7 +125,7 @@ impl Container {
|
|||
/// Establece el identificador único (`id`) del contenedor.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +139,7 @@ impl Container {
|
|||
/// - Redondear las esquinas ([`classes::Rounded`]).
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -146,39 +150,11 @@ impl Container {
|
|||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente hijo al contenedor.
|
||||
#[inline]
|
||||
pub fn add_child(mut self, component: impl Component) -> Self {
|
||||
self.children.add(Child::with(component));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
||||
/// Añade un nuevo componente al contenedor o modifica la lista de componentes (`children`) con
|
||||
/// una operación [`ChildOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_child(mut self, op: ChildOp) -> Self {
|
||||
self.children.alter_child(op);
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
|
||||
// **< Container GETTERS >**********************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al contenedor.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el tipo semántico del contenedor.
|
||||
pub fn container_kind(&self) -> &container::Kind {
|
||||
&self.container_kind
|
||||
}
|
||||
|
||||
/// Devuelve el comportamiento para el ancho del contenedor.
|
||||
pub fn width(&self) -> &container::Width {
|
||||
&self.container_width
|
||||
}
|
||||
|
||||
/// Devuelve la lista de componentes (`children`) del contenedor.
|
||||
pub fn children(&self) -> &Children {
|
||||
&self.children
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ impl Width {
|
|||
} */
|
||||
|
||||
/// Devuelve la clase asociada al comportamiento del contenedor según el ajuste de su ancho.
|
||||
#[inline]
|
||||
pub fn to_class(self) -> String {
|
||||
match self {
|
||||
Self::Default => BreakPoint::None.class_with(Self::CONTAINER, ""),
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
//! .with_button_color(ButtonColor::Background(Color::Secondary))
|
||||
//! .with_auto_close(dropdown::AutoClose::ClickableInside)
|
||||
//! .with_direction(dropdown::Direction::Dropend)
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://www.google.es"))
|
||||
//! .add_item(dropdown::Item::divider())
|
||||
//! .add_item(dropdown::Item::header(L10n::n("User session")))
|
||||
//! .add_item(dropdown::Item::button(L10n::n("Sign out")));
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(dropdown::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(dropdown::Item::divider())
|
||||
//! .with_item(dropdown::Item::header(L10n::n("User session")))
|
||||
//! .with_item(dropdown::Item::button(L10n::n("Sign out")));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
|
|
|
|||
|
|
@ -19,53 +19,64 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`dropdown`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Dropdown {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
title : L10n,
|
||||
button_size : ButtonSize,
|
||||
button_color : ButtonColor,
|
||||
button_split : bool,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al menú desplegable.
|
||||
classes: Classes,
|
||||
/// Devuelve el título del menú desplegable.
|
||||
title: L10n,
|
||||
/// Devuelve el tamaño configurado del botón.
|
||||
button_size: ButtonSize,
|
||||
/// Devuelve el color/estilo configurado del botón.
|
||||
button_color: ButtonColor,
|
||||
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
|
||||
button_split: bool,
|
||||
/// Devuelve si el botón del menú está integrado en un grupo de botones.
|
||||
button_grouped: bool,
|
||||
auto_close : dropdown::AutoClose,
|
||||
direction : dropdown::Direction,
|
||||
menu_align : dropdown::MenuAlign,
|
||||
menu_position : dropdown::MenuPosition,
|
||||
items : Children,
|
||||
/// Devuelve la política de cierre automático del menú desplegado.
|
||||
auto_close: dropdown::AutoClose,
|
||||
/// Devuelve la dirección de despliegue configurada.
|
||||
direction: dropdown::Direction,
|
||||
/// Devuelve la configuración de alineación horizontal del menú desplegable.
|
||||
menu_align: dropdown::MenuAlign,
|
||||
/// Devuelve la posición configurada para el menú desplegable.
|
||||
menu_position: dropdown::MenuPosition,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
impl Component for Dropdown {
|
||||
fn new() -> Self {
|
||||
Dropdown::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(
|
||||
ClassesOp::Prepend,
|
||||
self.direction().class_with(self.button_grouped()),
|
||||
self.direction().class_with(*self.button_grouped()),
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
// Si no hay elementos en el menú, no se prepara.
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// Título opcional para el menú desplegable.
|
||||
let title = self.title().using(cx);
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
div id=[self.id()] class=[self.classes().get()] {
|
||||
@if !title.is_empty() {
|
||||
@let mut btn_classes = AttrClasses::new({
|
||||
@let mut btn_classes = Classes::new({
|
||||
let mut classes = "btn".to_string();
|
||||
self.button_size().push_class(&mut classes);
|
||||
self.button_color().push_class(&mut classes);
|
||||
|
|
@ -75,14 +86,14 @@ impl Component for Dropdown {
|
|||
@let offset = pos.data_offset();
|
||||
@let reference = pos.data_reference();
|
||||
@let auto_close = self.auto_close.as_str();
|
||||
@let menu_classes = AttrClasses::new({
|
||||
@let menu_classes = Classes::new({
|
||||
let mut classes = "dropdown-menu".to_string();
|
||||
self.menu_align().push_class(&mut classes);
|
||||
classes
|
||||
});
|
||||
|
||||
// Renderizado en modo split (dos botones) o simple (un botón).
|
||||
@if self.button_split() {
|
||||
@if *self.button_split() {
|
||||
// Botón principal (acción/etiqueta).
|
||||
@let btn = html! {
|
||||
button
|
||||
|
|
@ -96,7 +107,7 @@ impl Component for Dropdown {
|
|||
@let btn_toggle = html! {
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.alter_value(
|
||||
class=[btn_classes.alter_classes(
|
||||
ClassesOp::Add, "dropdown-toggle dropdown-toggle-split"
|
||||
).get()]
|
||||
data-bs-toggle="dropdown"
|
||||
|
|
@ -127,7 +138,7 @@ impl Component for Dropdown {
|
|||
// Botón único con funcionalidad de *toggle*.
|
||||
button
|
||||
type="button"
|
||||
class=[btn_classes.alter_value(
|
||||
class=[btn_classes.alter_classes(
|
||||
ClassesOp::Add, "dropdown-toggle"
|
||||
).get()]
|
||||
data-bs-toggle="dropdown"
|
||||
|
|
@ -155,14 +166,14 @@ impl Dropdown {
|
|||
/// Establece el identificador único (`id`) del menú desplegable.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al menú desplegable.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -229,74 +240,22 @@ impl Dropdown {
|
|||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo elemento hijo al menú.
|
||||
#[inline]
|
||||
pub fn add_item(mut self, item: dropdown::Item) -> Self {
|
||||
self.items.add(Child::with(item));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de elementos (`children`) aplicando una operación [`TypedOp`].
|
||||
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
|
||||
/// [`ChildOp`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// dropdown.with_item(dropdown::Item::link("Opción", "/ruta"));
|
||||
/// dropdown.with_item(ChildOp::AddMany(vec![
|
||||
/// dropdown::Item::link(...).into(),
|
||||
/// dropdown::Item::divider().into(),
|
||||
/// dropdown::Item::link(...).into(),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
pub fn with_items(mut self, op: TypedOp<dropdown::Item>) -> Self {
|
||||
self.items.alter_typed(op);
|
||||
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.items.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
|
||||
// **< Dropdown GETTERS >***********************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al menú desplegable.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el título del menú desplegable.
|
||||
pub fn title(&self) -> &L10n {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Devuelve el tamaño configurado del botón.
|
||||
pub fn button_size(&self) -> &ButtonSize {
|
||||
&self.button_size
|
||||
}
|
||||
|
||||
/// Devuelve el color/estilo configurado del botón.
|
||||
pub fn button_color(&self) -> &ButtonColor {
|
||||
&self.button_color
|
||||
}
|
||||
|
||||
/// Devuelve si se debe desdoblar (*split*) el botón (botón de acción + *toggle*).
|
||||
pub fn button_split(&self) -> bool {
|
||||
self.button_split
|
||||
}
|
||||
|
||||
/// Devuelve si el botón del menú está integrado en un grupo de botones.
|
||||
pub fn button_grouped(&self) -> bool {
|
||||
self.button_grouped
|
||||
}
|
||||
|
||||
/// Devuelve la política de cierre automático del menú desplegado.
|
||||
pub fn auto_close(&self) -> &dropdown::AutoClose {
|
||||
&self.auto_close
|
||||
}
|
||||
|
||||
/// Devuelve la dirección de despliegue configurada.
|
||||
pub fn direction(&self) -> &dropdown::Direction {
|
||||
&self.direction
|
||||
}
|
||||
|
||||
/// Devuelve la configuración de alineación horizontal del menú desplegable.
|
||||
pub fn menu_align(&self) -> &dropdown::MenuAlign {
|
||||
&self.menu_align
|
||||
}
|
||||
|
||||
/// Devuelve la posición configurada para el menú desplegable.
|
||||
pub fn menu_position(&self) -> &dropdown::MenuPosition {
|
||||
&self.menu_position
|
||||
}
|
||||
|
||||
/// Devuelve la lista de elementos (`children`) del menú.
|
||||
pub fn items(&self) -> &Children {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,19 @@ use pagetop::prelude::*;
|
|||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
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.
|
||||
/// Elemento de navegación basado en una [`RoutePath`] dinámica devuelta por
|
||||
/// [`FnPathByContext`]. Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente
|
||||
/// deshabilitado.
|
||||
Link {
|
||||
label: L10n,
|
||||
path: FnPathByContext,
|
||||
route: FnPathByContext,
|
||||
blank: bool,
|
||||
disabled: bool,
|
||||
},
|
||||
|
|
@ -40,46 +41,48 @@ pub enum ItemKind {
|
|||
/// visible que puede comportarse como texto, enlace, botón, encabezado o separador, 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)]
|
||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al elemento.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.item_kind() {
|
||||
ItemKind::Void => PrepareMarkup::None,
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.item_kind() {
|
||||
ItemKind::Void => html! {},
|
||||
|
||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||
ItemKind::Label(label) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
span class="dropdown-item-text" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let path = path(cx);
|
||||
let route_link = route(cx);
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = !*disabled && (current_path == Some(path));
|
||||
let is_current = !*disabled && (current_path == Some(route_link.path()));
|
||||
|
||||
let mut classes = "dropdown-item".to_string();
|
||||
if is_current {
|
||||
|
|
@ -89,15 +92,15 @@ impl Component for Item {
|
|||
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 href = (!*disabled).then_some(route_link);
|
||||
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");
|
||||
let tabindex = disabled.then_some("-1");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class=(classes)
|
||||
|
|
@ -111,7 +114,7 @@ impl Component for Item {
|
|||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Button { label, disabled } => {
|
||||
|
|
@ -123,7 +126,7 @@ impl Component for Item {
|
|||
let aria_disabled = disabled.then_some("true");
|
||||
let disabled_attr = disabled.then_some("disabled");
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
button
|
||||
class=(classes)
|
||||
|
|
@ -134,39 +137,43 @@ impl Component for Item {
|
|||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Header(label) => PrepareMarkup::With(html! {
|
||||
ItemKind::Header(label) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
h6 class="dropdown-header" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
ItemKind::Divider => PrepareMarkup::With(html! {
|
||||
ItemKind::Divider => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] { hr class="dropdown-divider" {} }
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
///
|
||||
/// La ruta se obtiene invocando [`FnPathByContext`], que devuelve dinámicamente una
|
||||
/// [`RoutePath`] en función del [`Context`]. El enlace se marca como `active` si la ruta actual
|
||||
/// del *request* coincide con la ruta de destino (devuelta por `RoutePath::path`).
|
||||
pub fn link(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
|
|
@ -175,11 +182,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: true,
|
||||
},
|
||||
|
|
@ -188,11 +195,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_blank(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: false,
|
||||
},
|
||||
|
|
@ -201,11 +208,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_blank_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
|
|
@ -215,7 +222,7 @@ impl Item {
|
|||
|
||||
/// Crea un botón de acción local, sin navegación asociada.
|
||||
pub fn button(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: false,
|
||||
|
|
@ -226,7 +233,7 @@ impl Item {
|
|||
|
||||
/// Crea un botón deshabilitado.
|
||||
pub fn button_disabled(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Button {
|
||||
label,
|
||||
disabled: true,
|
||||
|
|
@ -237,7 +244,7 @@ impl Item {
|
|||
|
||||
/// Crea un encabezado para un grupo de elementos dentro del menú.
|
||||
pub fn header(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Header(label),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -245,7 +252,7 @@ impl Item {
|
|||
|
||||
/// Crea un separador visual entre bloques de elementos.
|
||||
pub fn divider() -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Divider,
|
||||
..Default::default()
|
||||
}
|
||||
|
|
@ -256,26 +263,14 @@ impl Item {
|
|||
/// 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.id.alter_id(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.classes.alter_classes(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.
|
||||
pub fn item_kind(&self) -> &ItemKind {
|
||||
&self.item_kind
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ pub enum AutoClose {
|
|||
}
|
||||
|
||||
impl AutoClose {
|
||||
// Devuelve el valor para `data-bs-auto-close`, o `None` si es el comportamiento por defecto.
|
||||
/// Devuelve el valor para `data-bs-auto-close`, o `None` si es el comportamiento por defecto.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
pub(crate) const fn as_str(self) -> Option<&'static str> {
|
||||
|
|
@ -59,7 +59,7 @@ pub enum Direction {
|
|||
}
|
||||
|
||||
impl Direction {
|
||||
// Mapea la dirección teniendo en cuenta si se agrupa con otros menús [`Dropdown`].
|
||||
/// Mapea la dirección teniendo en cuenta si se agrupa con otros menús [`Dropdown`].
|
||||
#[rustfmt::skip ]
|
||||
#[inline]
|
||||
const fn as_str(self, grouped: bool) -> &'static str {
|
||||
|
|
@ -74,8 +74,8 @@ impl Direction {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la dirección de despliegue a la cadena de clases teniendo en cuenta si se agrupa con
|
||||
// otros menús [`Dropdown`].
|
||||
/// Añade la dirección de despliegue a la cadena de clases teniendo en cuenta si se agrupa con
|
||||
/// otros menús [`Dropdown`].
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String, grouped: bool) {
|
||||
if grouped {
|
||||
|
|
@ -93,10 +93,10 @@ impl Direction {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve la clase asociada a la dirección teniendo en cuenta si se agrupa con otros menús
|
||||
// [`Dropdown`], o `""` si no corresponde ninguna.
|
||||
#[inline]
|
||||
pub(crate) fn class_with(self, grouped: bool) -> String {
|
||||
/// Devuelve la clase asociada a la dirección teniendo en cuenta si se agrupa con otros menús
|
||||
/// [`Dropdown`], o `""` si no corresponde ninguna.
|
||||
#[doc(hidden)]
|
||||
pub fn class_with(self, grouped: bool) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes, grouped);
|
||||
classes
|
||||
|
|
@ -138,7 +138,7 @@ impl MenuAlign {
|
|||
classes.push_str(class);
|
||||
}
|
||||
|
||||
// Añade las clases de alineación a `classes` (sin incluir la base `dropdown-menu`).
|
||||
/// Añade las clases de alineación a `classes` (sin incluir la base `dropdown-menu`).
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
match self {
|
||||
|
|
@ -179,8 +179,7 @@ impl MenuAlign {
|
|||
}
|
||||
|
||||
/* Devuelve las clases de alineación sin incluir `dropdown-menu` (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
pub fn to_class(self) -> String {
|
||||
let mut classes = String::new();
|
||||
self.push_class(&mut classes);
|
||||
classes
|
||||
|
|
@ -206,7 +205,7 @@ pub enum MenuPosition {
|
|||
}
|
||||
|
||||
impl MenuPosition {
|
||||
// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
|
||||
/// Devuelve el valor para `data-bs-offset` o `None` si no aplica.
|
||||
#[inline]
|
||||
pub(crate) fn data_offset(self) -> Option<String> {
|
||||
match self {
|
||||
|
|
@ -215,7 +214,7 @@ impl MenuPosition {
|
|||
}
|
||||
}
|
||||
|
||||
// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
|
||||
/// Devuelve el valor para `data-bs-reference` o `None` si no aplica.
|
||||
#[inline]
|
||||
pub(crate) fn data_reference(self) -> Option<&'static str> {
|
||||
match self {
|
||||
|
|
|
|||
62
extensions/pagetop-bootsier/src/theme/form.rs
Normal file
62
extensions/pagetop-bootsier/src/theme/form.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
//! Definiciones para crear formularios ([`Form`]).
|
||||
//!
|
||||
//! # Ejemplo
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pagetop::prelude::*;
|
||||
//! use pagetop_bootsier::prelude::*;
|
||||
//!
|
||||
//! let form_login = Form::new()
|
||||
//! .with_id("login")
|
||||
//! .with_action("/login")
|
||||
//! .with_child(
|
||||
//! form::input::Field::email()
|
||||
//! .with_name("email")
|
||||
//! .with_label(L10n::n("Email"))
|
||||
//! .with_required(true),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! form::input::Field::password()
|
||||
//! .with_name("password")
|
||||
//! .with_label(L10n::n("Password"))
|
||||
//! .with_required(true),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! form::Checkbox::check()
|
||||
//! .with_name("remember")
|
||||
//! .with_label(L10n::n("Remember me")),
|
||||
//! )
|
||||
//! .with_child(
|
||||
//! Button::submit(L10n::n("Sign in"))
|
||||
//! .with_color(ButtonColor::Background(Color::Primary)),
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
pub use props::{Autocomplete, AutofillField, CheckboxKind, Method};
|
||||
|
||||
mod component;
|
||||
pub use component::Form;
|
||||
|
||||
mod fieldset;
|
||||
pub use fieldset::Fieldset;
|
||||
|
||||
mod checkbox;
|
||||
pub use checkbox::Checkbox;
|
||||
|
||||
pub mod check;
|
||||
|
||||
pub mod radio;
|
||||
|
||||
pub mod select;
|
||||
|
||||
pub mod input;
|
||||
|
||||
mod textarea;
|
||||
pub use textarea::Textarea;
|
||||
|
||||
mod range;
|
||||
pub use range::Range;
|
||||
|
||||
mod hidden;
|
||||
pub use hidden::Hidden;
|
||||
257
extensions/pagetop-bootsier/src/theme/form/check.rs
Normal file
257
extensions/pagetop-bootsier/src/theme/form/check.rs
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
//! Definiciones para crear grupos de casillas de verificación (*check buttons*).
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Casilla de verificación individual de un [`form::check::Field`](Field).
|
||||
///
|
||||
/// Representa cada casilla de un grupo de casillas de verificación, con una etiqueta localizable
|
||||
/// visible. Puede marcarse como seleccionada o deshabilitada de forma independiente al resto.
|
||||
///
|
||||
/// El parámetro `name` de [`form::check::Item::new()`](Item::new) se combina con el `name` del
|
||||
/// grupo para componer el atributo `name` de la casilla. Por ejemplo, si el grupo tiene
|
||||
/// `name=interests` y el ítem se crea con `name=tech`, la casilla tendrá `name=interests_tech`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let item = form::check::Item::new("apple", L10n::n("Apple")).with_checked(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
/// Devuelve el nombre que se combina con el del grupo para componer el atributo `name`.
|
||||
name: AttrValue,
|
||||
/// Devuelve la etiqueta de la casilla.
|
||||
label: L10n,
|
||||
/// Devuelve si la casilla debe aparecer marcada por defecto.
|
||||
checked: bool,
|
||||
/// Devuelve si la casilla está deshabilitada.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea una nueva casilla con el nombre y la etiqueta indicados.
|
||||
///
|
||||
/// El parámetro `name` se combina con el del grupo para componer el atributo `name` de la
|
||||
/// casilla.
|
||||
pub fn new(name: impl AsRef<str>, label: L10n) -> Self {
|
||||
Self {
|
||||
name: AttrValue::new(name),
|
||||
label,
|
||||
checked: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// Establece si la casilla debe aparecer marcada por defecto.
|
||||
pub fn with_checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la casilla está deshabilitada.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// **< Field >**************************************************************************************
|
||||
|
||||
/// Componente para crear un **grupo de casillas de verificación**.
|
||||
///
|
||||
/// Renderiza un conjunto de casillas de verificación donde, a diferencia de un grupo de botones
|
||||
/// [`form::radio::Field`](crate::theme::form::radio::Field), cada casilla puede marcarse de forma
|
||||
/// independiente.
|
||||
///
|
||||
/// Las casillas se añaden mediante [`with_item()`](Field::with_item) usando instancias de
|
||||
/// [`form::check::Item`](Item). Si se activa el modo en línea con
|
||||
/// [`with_inline()`](Field::with_inline), las casillas se disponen horizontalmente.
|
||||
///
|
||||
/// El atributo `name` de cada casilla se construye automáticamente combinando el `name` del grupo
|
||||
/// y el `name` del [`form::check::Item`](Item) con un guion bajo. Por ejemplo, para el grupo con
|
||||
/// `name=interests` y casillas con `name=art` y `name=tech`, se genera `name=interests_art` y
|
||||
/// `name=interests_tech`.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let interests = form::check::Field::new()
|
||||
/// .with_name("interests")
|
||||
/// .with_label(L10n::n("Areas of interest"))
|
||||
/// .with_item(form::check::Item::new("art", L10n::n("Art")))
|
||||
/// .with_item(form::check::Item::new("tech", L10n::n("Technology")))
|
||||
/// .with_item(form::check::Item::new("science", L10n::n("Science")).with_checked(true));
|
||||
/// ```
|
||||
///
|
||||
/// Cada `name` debe ser único y válido como identificador de campo. Cuando el usuario marca una
|
||||
/// casilla, el navegador envía algo como `interests_tech=true`; mientras que si no la marca, no
|
||||
/// envía nada. En el servidor cada campo se deserializa como `bool` con `#[serde(default)]`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// #[serde(default)]
|
||||
/// interests_art: bool,
|
||||
/// #[serde(default)]
|
||||
/// interests_tech: bool,
|
||||
/// #[serde(default)]
|
||||
/// interests_science: bool,
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Field {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del grupo.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre base compartido por todas las casillas del grupo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del grupo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve el texto de ayuda del grupo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve las casillas del grupo.
|
||||
items: Vec<Item>,
|
||||
/// Devuelve si todo el grupo está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve si las casillas se muestran en línea horizontalmente.
|
||||
inline: bool,
|
||||
}
|
||||
|
||||
impl Component for Field {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-checkboxes");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let name = self
|
||||
.name()
|
||||
.get()
|
||||
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 3));
|
||||
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
|
||||
Ok(html! {
|
||||
div id=(&container_id) class=[self.classes().get()] {
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label class="form-label" { (label) }
|
||||
}
|
||||
@let item_classes = if *self.inline() {
|
||||
"form-check form-check-inline"
|
||||
} else {
|
||||
"form-check"
|
||||
};
|
||||
@for (item, i) in self.items().iter().zip(1..) {
|
||||
@let i = i.to_string();
|
||||
@let item_id = util::join!(&container_id, "-check-", &i);
|
||||
@let item_name = if let Some(item_name) = item.name().get() {
|
||||
util::join!(&name, "_", &item_name)
|
||||
} else {
|
||||
util::join!(&name, "_", &i)
|
||||
};
|
||||
div class=(item_classes) {
|
||||
input
|
||||
type="checkbox"
|
||||
id=(&item_id)
|
||||
class="form-check-input"
|
||||
name=(&item_name)
|
||||
value="true"
|
||||
checked[*item.checked()]
|
||||
disabled[*item.disabled() || *self.disabled()];
|
||||
label class="form-check-label" for=(&item_id) {
|
||||
(item.label().using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
// **< Field BUILDER >**************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del grupo de casillas.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del grupo de casillas.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre base para el grupo de casillas.
|
||||
///
|
||||
/// Se combina con el `name` de cada [`form::check::Item`](Item) para generar el atributo `name`
|
||||
/// de cada casilla de verificación. Por ejemplo, con `name=interests` en el grupo y `name=tech`
|
||||
/// en el ítem, se genera `name=interests_tech`.
|
||||
///
|
||||
/// Si se omite, se asigna un nombre generado automáticamente. Para deserializar los campos en
|
||||
/// el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade una casilla al grupo. Las casillas se muestran en el orden en que se añaden.
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, item: Item) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si todo el grupo está deshabilitado.
|
||||
///
|
||||
/// Cuando está activo, se combina con el estado `disabled` de cada [`Item`].
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si las casillas se muestran en línea horizontalmente.
|
||||
///
|
||||
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor de cada casilla.
|
||||
#[builder_fn]
|
||||
pub fn with_inline(mut self, inline: bool) -> Self {
|
||||
self.inline = inline;
|
||||
self
|
||||
}
|
||||
}
|
||||
232
extensions/pagetop-bootsier/src/theme/form/checkbox.rs
Normal file
232
extensions/pagetop-bootsier/src/theme/form/checkbox.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Componente para crear una **casilla de verificación** o un **interruptor** (*toggle switch*).
|
||||
///
|
||||
/// Renderiza un control binario (marcado/no marcado) en dos variantes visuales, por defecto se
|
||||
/// muestra como una casilla de verificación estándar, pero también puede renderizarse como un
|
||||
/// interruptor de encendido/apagado ([`Checkbox::switch()`]).
|
||||
///
|
||||
/// Se puede mostrar en línea con otros controles usando [`with_inline()`](Checkbox::with_inline), o
|
||||
/// justificar a la derecha del contenedor invirtiendo el orden de la etiqueta y el control usando
|
||||
/// [`with_reverse()`](Checkbox::with_reverse).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let accept_terms = form::Checkbox::check() // También sirve new() o default().
|
||||
/// .with_name("terms_accepted")
|
||||
/// .with_label(L10n::n("I accept the terms and conditions"))
|
||||
/// .with_required(true);
|
||||
///
|
||||
/// let notifications = form::Checkbox::switch()
|
||||
/// .with_name("notifications_enabled")
|
||||
/// .with_label(L10n::n("Receive email notifications"))
|
||||
/// .with_checked(true);
|
||||
/// ```
|
||||
///
|
||||
/// Cuando el control está activo, el navegador envía `name=true`; si no lo está, no envía nada.
|
||||
/// En el servidor el campo se deserializa como `bool` con `#[serde(default)]`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// #[serde(default)]
|
||||
/// terms_accepted: bool, // true = marcada, false = no marcada.
|
||||
/// #[serde(default)]
|
||||
/// notifications_enabled: bool, // true = activo, false = inactivo.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Checkbox {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del control.
|
||||
classes: Classes,
|
||||
/// Devuelve la variante visual del control.
|
||||
checkbox_kind: form::CheckboxKind,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del control.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si el control debe estar marcado/activo por defecto.
|
||||
checked: bool,
|
||||
/// Devuelve si el control recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el campo es obligatorio.
|
||||
required: bool,
|
||||
/// Devuelve si el control está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve si el control se muestra en línea con otros controles.
|
||||
inline: bool,
|
||||
/// Devuelve si el control y su etiqueta se justifican a la derecha del contenedor.
|
||||
reverse: bool,
|
||||
}
|
||||
|
||||
impl Component for Checkbox {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
let mut classes = "form-field form-check".to_string();
|
||||
if *self.checkbox_kind() == form::CheckboxKind::Switch {
|
||||
classes.push_str(" form-switch");
|
||||
}
|
||||
if *self.inline() {
|
||||
classes.push_str(" form-check-inline");
|
||||
}
|
||||
if *self.reverse() {
|
||||
classes.push_str(" form-check-reverse");
|
||||
}
|
||||
self.alter_classes(ClassesOp::Prepend, classes);
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let name = self
|
||||
.name()
|
||||
.get()
|
||||
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 1));
|
||||
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
|
||||
let checkbox_id = util::join!(&container_id, "-checkbox");
|
||||
let is_switch = *self.checkbox_kind() == form::CheckboxKind::Switch;
|
||||
Ok(html! {
|
||||
div id=(&container_id) class=[self.classes().get()] {
|
||||
input
|
||||
type="checkbox"
|
||||
role=[is_switch.then_some("switch")]
|
||||
id=(&checkbox_id)
|
||||
class="form-check-input"
|
||||
name=(&name)
|
||||
value="true"
|
||||
checked[*self.checked()]
|
||||
autofocus[*self.autofocus()]
|
||||
required[*self.required()]
|
||||
disabled[*self.disabled()];
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label class="form-check-label" for=(&checkbox_id) {
|
||||
(label)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Checkbox {
|
||||
/// Crea una casilla de verificación estándar.
|
||||
pub fn check() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Crea un interruptor de encendido/apagado (*toggle switch*).
|
||||
pub fn switch() -> Self {
|
||||
Self {
|
||||
checkbox_kind: form::CheckboxKind::Switch,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Checkbox BUILDER >***********************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del control.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del control.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la variante visual del control.
|
||||
#[builder_fn]
|
||||
pub fn with_kind(mut self, kind: form::CheckboxKind) -> Self {
|
||||
self.checkbox_kind = kind;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Si se omite, se asigna un identificador generado automáticamente. Para deserializar el campo
|
||||
/// en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del control (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control debe aparecer marcado/activo por defecto.
|
||||
#[builder_fn]
|
||||
pub fn with_checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control se muestra en línea con otros controles.
|
||||
///
|
||||
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor, lo que permite
|
||||
/// alinear varios controles horizontalmente.
|
||||
#[builder_fn]
|
||||
pub fn with_inline(mut self, inline: bool) -> Self {
|
||||
self.inline = inline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control y su etiqueta se justifican a la derecha del contenedor.
|
||||
///
|
||||
/// Al activar este modo, se añade la clase `form-check-reverse` al contenedor.
|
||||
#[builder_fn]
|
||||
pub fn with_reverse(mut self, reverse: bool) -> Self {
|
||||
self.reverse = reverse;
|
||||
self
|
||||
}
|
||||
}
|
||||
127
extensions/pagetop-bootsier/src/theme/form/component.rs
Normal file
127
extensions/pagetop-bootsier/src/theme/form/component.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
|
||||
/// Componente para crear un **formulario**.
|
||||
///
|
||||
/// Este componente renderiza un `<form>` estándar con soporte para los atributos más habituales:
|
||||
///
|
||||
/// - `id`: identificador opcional del formulario.
|
||||
/// - `classes`: clases CSS adicionales (p. ej. utilidades CSS).
|
||||
/// - `action`: URL/ruta de destino para el envío.
|
||||
/// - `method`: método usado por el formulario para el envío de los datos (ver explicaciones en
|
||||
/// [`form::Method`](crate::theme::form::Method)).
|
||||
/// - `accept-charset`: juego de caracteres aceptado (por defecto es `"UTF-8"`).
|
||||
/// - `children`: contenido del formulario.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let search = Form::new()
|
||||
/// .with_id("search")
|
||||
/// .with_action("/search")
|
||||
/// .with_method(form::Method::Get)
|
||||
/// .with_child(form::input::Field::search().with_name("q"));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Form {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del formulario.
|
||||
classes: Classes,
|
||||
/// Devuelve la URL/ruta de destino del formulario.
|
||||
action: AttrValue,
|
||||
/// Devuelve el método para enviar el formulario.
|
||||
method: form::Method,
|
||||
/// Devuelve el juego de caracteres aceptado por el formulario.
|
||||
#[default(_code = "AttrValue::new(\"UTF-8\")")]
|
||||
charset: AttrValue,
|
||||
/// Devuelve la lista de componentes del formulario.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Form {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let method = match self.method() {
|
||||
form::Method::Post => Some("post"),
|
||||
form::Method::Get => None,
|
||||
};
|
||||
Ok(html! {
|
||||
form
|
||||
id=[self.id()]
|
||||
class=[self.classes().get()]
|
||||
action=[self.action().get()]
|
||||
method=[method]
|
||||
accept-charset=[self.charset().get()]
|
||||
{
|
||||
(self.children().render(cx))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Form {
|
||||
// **< Form BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la URL/ruta de destino del formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_action(mut self, action: impl AsRef<str>) -> Self {
|
||||
self.action.alter_str(action);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el método para enviar el formulario.
|
||||
///
|
||||
/// - `GET`: el atributo `method` se omite.
|
||||
/// - `POST`: se establece `method="post"`.
|
||||
#[builder_fn]
|
||||
pub fn with_method(mut self, method: form::Method) -> Self {
|
||||
self.method = method;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el juego de caracteres aceptado por el formulario.
|
||||
///
|
||||
/// Por defecto se utiliza `"UTF-8"`.
|
||||
#[builder_fn]
|
||||
pub fn with_charset(mut self, charset: impl AsRef<str>) -> Self {
|
||||
self.charset.alter_str(charset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente al formulario o modifica la lista de componentes (`children`) con
|
||||
/// una operación [`ChildOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
116
extensions/pagetop-bootsier/src/theme/form/fieldset.rs
Normal file
116
extensions/pagetop-bootsier/src/theme/form/fieldset.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Componente para crear un **grupo de controles relacionados** en un formulario.
|
||||
///
|
||||
/// Renderiza un `<fieldset>` con una leyenda opcional que sirve de encabezado y una descripción
|
||||
/// también opcional que aparece justo antes de los controles. Es un elemento semántico que mejora
|
||||
/// la accesibilidad porque los lectores de pantalla anuncian la leyenda antes de leer cada control
|
||||
/// del contenido.
|
||||
///
|
||||
/// Los componentes del grupo se añaden con [`with_child()`](Fieldset::with_child). Si no hay
|
||||
/// contenido para renderizar, el `fieldset` no se genera. Si está deshabilitado, todos sus
|
||||
/// controles hijos quedan deshabilitados automáticamente por el navegador.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let personal_data = form::Fieldset::new()
|
||||
/// .with_legend(L10n::n("Personal data"))
|
||||
/// .with_description(L10n::n("Enter your full name and contact email."))
|
||||
/// .with_child(form::input::Field::text().with_name("name").with_label(L10n::n("Full name")))
|
||||
/// .with_child(form::input::Field::email().with_name("email").with_label(L10n::n("Email")));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Fieldset {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del `fieldset`.
|
||||
classes: Classes,
|
||||
/// Devuelve la leyenda del `fieldset`.
|
||||
legend: Attr<L10n>,
|
||||
/// Devuelve la descripción del `fieldset`.
|
||||
description: Attr<L10n>,
|
||||
/// Devuelve si el `fieldset` está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve la lista de componentes del `fieldset`.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Fieldset {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let children = self.children().render(cx);
|
||||
|
||||
if children.is_empty() {
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
Ok(html! {
|
||||
fieldset id=[self.id()] class=[self.classes().get()] disabled[*self.disabled()] {
|
||||
@if let Some(legend) = self.legend().lookup(cx) {
|
||||
legend { (legend) }
|
||||
}
|
||||
@if let Some(description) = self.description().lookup(cx) {
|
||||
p class="fieldset-description" { (description) }
|
||||
}
|
||||
(children)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Fieldset {
|
||||
// **< Fieldset BUILDER >***********************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del `fieldset` (grupo de controles).
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al `fieldset`.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la leyenda del `fieldset` (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_legend(mut self, legend: impl Into<Option<L10n>>) -> Self {
|
||||
self.legend.alter_opt(legend.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la descripción del `fieldset` (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_description(mut self, description: impl Into<Option<L10n>>) -> Self {
|
||||
self.description.alter_opt(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el `fieldset` está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente al `fieldset`, o aplica una operación [`ChildOp`] sobre la lista
|
||||
/// de componentes (`children`).
|
||||
#[builder_fn]
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
72
extensions/pagetop-bootsier/src/theme/form/hidden.rs
Normal file
72
extensions/pagetop-bootsier/src/theme/form/hidden.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Componente para crear un **campo oculto** del formulario.
|
||||
///
|
||||
/// Renderiza un campo sin ningún marcado visible. Su valor se envía al servidor junto con el resto
|
||||
/// del formulario, pero el usuario no puede verlo ni modificarlo.
|
||||
///
|
||||
/// Es útil para transportar datos de estado, tokens CSRF, identificadores o cualquier valor que
|
||||
/// deba incluirse en el envío sin ser accesible al usuario.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let token = form::Hidden::new()
|
||||
/// .with_name("csrf_token")
|
||||
/// .with_value("a1b2c3d4e5");
|
||||
/// ```
|
||||
///
|
||||
/// Al enviar el formulario el navegador transmite `name=valor`. En el servidor se deserializa
|
||||
/// como `String`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// csrf_token: String,
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Hidden {
|
||||
/// Devuelve el nombre del campo oculto.
|
||||
name: AttrName,
|
||||
/// Devuelve el valor del campo oculto.
|
||||
value: AttrValue,
|
||||
}
|
||||
|
||||
impl Component for Hidden {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn prepare(&self, _cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(html! {
|
||||
input
|
||||
type="hidden"
|
||||
name=[self.name().get()]
|
||||
value=[self.value().get()];
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Hidden {
|
||||
// **< Hidden BUILDER >*************************************************************************
|
||||
|
||||
/// Establece el nombre del campo oculto (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor del campo oculto (atributo `value`).
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
|
||||
self.value.alter_str(value);
|
||||
self
|
||||
}
|
||||
}
|
||||
453
extensions/pagetop-bootsier/src/theme/form/input.rs
Normal file
453
extensions/pagetop-bootsier/src/theme/form/input.rs
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
//! Definiciones para crear campos de texto de una línea.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
// **< Kind >***************************************************************************************
|
||||
|
||||
/// Tipo de campo para un [`form::input::Field`].
|
||||
///
|
||||
/// Determina el tipo de entrada que acepta, así como el comportamiento del navegador al interactuar
|
||||
/// con el campo. Implícitamente se aplica al crear el control: [`text()`](Field::text),
|
||||
/// [`password()`](Field::password), [`search()`](Field::search), [`email()`](Field::email),
|
||||
/// [`telephone()`](Field::telephone) o [`url()`](Field::url).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Kind {
|
||||
/// Entrada de texto genérico (`type="text"`). Es el tipo por defecto.
|
||||
#[default]
|
||||
Text,
|
||||
/// Entrada de una contraseña (`type="password"`). El contenido aparece enmascarado.
|
||||
Password,
|
||||
/// Campo de búsqueda (`type="search"`). Es un tipo semántico para los cuadros de búsqueda.
|
||||
Search,
|
||||
/// Entrada de un correo electrónico (`type="email"`). Permite validar el formato del correo.
|
||||
Email,
|
||||
/// Entrada de un teléfono (`type="tel"`). Activa el teclado de llamadas en móviles.
|
||||
Telephone,
|
||||
/// Entrada de una URL (`type="url"`). Comprueba que la entrada sea una URL bien formada.
|
||||
Url,
|
||||
}
|
||||
|
||||
impl fmt::Display for Kind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Kind::Text => "text",
|
||||
Kind::Password => "password",
|
||||
Kind::Search => "search",
|
||||
Kind::Email => "email",
|
||||
Kind::Telephone => "tel",
|
||||
Kind::Url => "url",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// **< Mode >***************************************************************************************
|
||||
|
||||
/// Sugerencia para el teclado virtual de un [`form::input::Field`].
|
||||
///
|
||||
/// Indica al navegador qué tipo de teclado virtual mostrar en dispositivos móviles o táctiles al
|
||||
/// editar el campo. A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los
|
||||
/// valores aceptados ni activa la validación del navegador; es sólo una sugerencia de presentación.
|
||||
///
|
||||
/// Se establece con [`form::input::Field::with_inputmode()`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Mode {
|
||||
/// Suprime el teclado virtual. Útil en campos con teclado personalizado basado en JavaScript.
|
||||
None,
|
||||
/// Teclado de texto genérico.
|
||||
Text,
|
||||
/// Teclado decimal, con dígitos y separador decimal.
|
||||
Decimal,
|
||||
/// Teclado numérico, con sólo dígitos.
|
||||
Numeric,
|
||||
/// Teclado de teléfono, con dígitos y símbolos `+`, `*` y `#`.
|
||||
Tel,
|
||||
/// Teclado optimizado para búsquedas (puede incluir tecla de búsqueda).
|
||||
Search,
|
||||
/// Teclado optimizado para correo electrónico (incluye `@` y `.`).
|
||||
Email,
|
||||
/// Teclado optimizado para URL (incluye `/`, `.` y `.com`).
|
||||
Url,
|
||||
}
|
||||
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Mode::None => "none",
|
||||
Mode::Text => "text",
|
||||
Mode::Decimal => "decimal",
|
||||
Mode::Numeric => "numeric",
|
||||
Mode::Tel => "tel",
|
||||
Mode::Search => "search",
|
||||
Mode::Email => "email",
|
||||
Mode::Url => "url",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// **< Field >**************************************************************************************
|
||||
|
||||
/// Componente para crear un **campo de texto de una línea**.
|
||||
///
|
||||
/// Renderiza los tipos más habituales en formularios:
|
||||
///
|
||||
/// - [`form::input::Field::text()`]: campo de texto genérico (`type="text"`, por defecto).
|
||||
/// - [`form::input::Field::password()`]: contraseña (`type="password"`).
|
||||
/// - [`form::input::Field::search()`]: búsqueda (`type="search"`).
|
||||
/// - [`form::input::Field::email()`]: correo electrónico (`type="email"`).
|
||||
/// - [`form::input::Field::telephone()`]: teléfono (`type="tel"`).
|
||||
/// - [`form::input::Field::url()`]: URL (`type="url"`).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let email = form::input::Field::email()
|
||||
/// .with_name("email")
|
||||
/// .with_label(L10n::n("Email address"))
|
||||
/// .with_placeholder(L10n::n("user@example.com"))
|
||||
/// .with_autocomplete(Some(form::Autocomplete::email()))
|
||||
/// .with_required(true);
|
||||
/// ```
|
||||
///
|
||||
/// Al enviar el formulario el navegador transmite `name=valor`. Un campo de texto siempre envía su
|
||||
/// valor, incluso si está vacío. En el servidor se deserializa como `String`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// email: String, // Siempre presente; cadena vacía si el usuario no escribió nada.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Field {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del campo.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo de campo.
|
||||
kind: Kind,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve el valor inicial del campo.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta del campo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si la etiqueta se muestra flotante sobre el campo.
|
||||
floating_label: bool,
|
||||
/// Devuelve el texto de ayuda del campo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve la longitud mínima permitida en caracteres.
|
||||
minlength: Attr<u16>,
|
||||
/// Devuelve la longitud máxima permitida en caracteres.
|
||||
maxlength: Attr<u16>,
|
||||
/// Devuelve el texto indicativo del campo.
|
||||
placeholder: Attr<L10n>,
|
||||
/// Devuelve la configuración de autocompletado del campo.
|
||||
autocomplete: Attr<form::Autocomplete>,
|
||||
/// Devuelve si el campo recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el campo es de sólo lectura.
|
||||
readonly: bool,
|
||||
/// Devuelve si el campo es obligatorio.
|
||||
required: bool,
|
||||
/// Devuelve si el campo está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve si el campo se muestra como texto plano sin bordes ni fondo.
|
||||
plaintext: bool,
|
||||
/// Devuelve la sugerencia de teclado virtual para el campo.
|
||||
inputmode: Attr<Mode>,
|
||||
}
|
||||
|
||||
impl Component for Field {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if *self.floating_label() {
|
||||
self.alter_classes(ClassesOp::Prepend, "form-floating");
|
||||
}
|
||||
self.alter_classes(
|
||||
ClassesOp::Prepend,
|
||||
util::join!("form-field form-field-", self.kind().to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let container_id = self
|
||||
.id()
|
||||
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
||||
let input_id = container_id.as_deref().map(|id| util::join!(id, "-input"));
|
||||
let input_class = if *self.plaintext() {
|
||||
"form-control-plaintext"
|
||||
} else {
|
||||
"form-control"
|
||||
};
|
||||
// La etiqueta flotante requiere el atributo `placeholder` para detectar cuándo el campo
|
||||
// está vacío y animar la etiqueta; si no está definido, se fuerza `placeholder=""`.
|
||||
let placeholder = if *self.floating_label() {
|
||||
Some(self.placeholder().lookup(cx).unwrap_or_default())
|
||||
} else {
|
||||
self.placeholder().lookup(cx)
|
||||
};
|
||||
let label = match self.label().lookup(cx) {
|
||||
Some(text) => html! {
|
||||
label for=[input_id.as_deref()] class="form-label" {
|
||||
(text)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => html! {},
|
||||
};
|
||||
Ok(html! {
|
||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||
@if !*self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
input
|
||||
type=(self.kind())
|
||||
id=[input_id.as_deref()]
|
||||
class=(input_class)
|
||||
name=[self.name().get()]
|
||||
value=[self.value().get()]
|
||||
minlength=[self.minlength().get()]
|
||||
maxlength=[self.maxlength().get()]
|
||||
placeholder=[placeholder]
|
||||
inputmode=[self.inputmode().get()]
|
||||
autocomplete=[self.autocomplete().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
readonly[*self.readonly() || *self.plaintext()]
|
||||
required[*self.required()]
|
||||
disabled[*self.disabled()];
|
||||
@if *self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
/// Crea un campo de **texto genérico** (`type="text"`).
|
||||
///
|
||||
/// Es el tipo por defecto. Adecuado para nombres, apellidos, ciudades y cualquier entrada
|
||||
/// textual sin restricciones de formato específicas.
|
||||
pub fn text() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Crea un campo de **contraseña** (`type="password"`).
|
||||
///
|
||||
/// El navegador oculta los caracteres introducidos. Se recomienda usar con
|
||||
/// [`with_autocomplete()`](Self::with_autocomplete) para permitir autorrellenar con una
|
||||
/// contraseña guardada o dejar al usuario recibir sugerencias o crear una nueva.
|
||||
pub fn password() -> Self {
|
||||
Self {
|
||||
kind: Kind::Password,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un campo de **búsqueda** (`type="search"`).
|
||||
///
|
||||
/// Semánticamente equivalente a `text` pero optimizado para búsquedas: algunos
|
||||
/// navegadores añaden un botón para borrar el contenido.
|
||||
pub fn search() -> Self {
|
||||
Self {
|
||||
kind: Kind::Search,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un campo de **correo electrónico** (`type="email"`).
|
||||
///
|
||||
/// El navegador valida el formato de la dirección antes de enviar el formulario. En
|
||||
/// dispositivos móviles muestra un teclado adaptado para introducir direcciones de correo.
|
||||
pub fn email() -> Self {
|
||||
Self {
|
||||
kind: Kind::Email,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un campo de **teléfono** (`type="tel"`).
|
||||
///
|
||||
/// No impone ninguna restricción de formato (los formatos de teléfono varían por país), pero
|
||||
/// en dispositivos móviles muestra el teclado numérico de llamadas.
|
||||
pub fn telephone() -> Self {
|
||||
Self {
|
||||
kind: Kind::Telephone,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un campo de **URL** (`type="url"`).
|
||||
///
|
||||
/// El navegador valida que el valor sea una URL bien formada antes de enviar el formulario.
|
||||
pub fn url() -> Self {
|
||||
Self {
|
||||
kind: Kind::Url,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Field BUILDER >**************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del contenedor del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor inicial del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
|
||||
self.value.alter_str(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la etiqueta se muestra flotante sobre el campo.
|
||||
///
|
||||
/// Cuando está activo, la etiqueta se superpone al campo y asciende al enfocarlo o cuando tiene
|
||||
/// contenido.
|
||||
#[builder_fn]
|
||||
pub fn with_floating_label(mut self, floating_label: bool) -> Self {
|
||||
self.floating_label = floating_label;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la longitud mínima permitida en caracteres (`None` para no imponer mínimo).
|
||||
#[builder_fn]
|
||||
pub fn with_minlength(mut self, minlength: Option<u16>) -> Self {
|
||||
self.minlength.alter_opt(minlength);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la longitud máxima permitida en caracteres (`None` para no imponer límite).
|
||||
#[builder_fn]
|
||||
pub fn with_maxlength(mut self, maxlength: Option<u16>) -> Self {
|
||||
self.maxlength.alter_opt(maxlength);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto indicativo del campo (`None` para quitarlo).
|
||||
///
|
||||
/// Este texto aparece en el mismo campo y desaparece en cuanto el usuario empieza a escribir.
|
||||
/// Al ser texto visible para el usuario se acepta [`L10n`] para poder localizarlo.
|
||||
#[builder_fn]
|
||||
pub fn with_placeholder(mut self, placeholder: impl Into<Option<L10n>>) -> Self {
|
||||
self.placeholder.alter_opt(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la configuración de autocompletado del campo.
|
||||
///
|
||||
/// Usar los métodos de [`form::Autocomplete`] para los valores más habituales (p. ej.
|
||||
/// [`Autocomplete::email()`](form::Autocomplete::email) o
|
||||
/// [`Autocomplete::current_password()`](form::Autocomplete::current_password)).
|
||||
#[builder_fn]
|
||||
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self {
|
||||
self.autocomplete.alter_opt(autocomplete);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es de sólo lectura.
|
||||
#[builder_fn]
|
||||
pub fn with_readonly(mut self, readonly: bool) -> Self {
|
||||
self.readonly = readonly;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo se muestra como texto plano (sin bordes ni fondo).
|
||||
///
|
||||
/// Útil para mostrar un valor no editable en pantalla que sí se envía al servidor con el
|
||||
/// formulario.
|
||||
#[builder_fn]
|
||||
pub fn with_plaintext(mut self, plaintext: bool) -> Self {
|
||||
self.plaintext = plaintext;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el modo de entrada sugerido para el teclado virtual en dispositivos móviles.
|
||||
///
|
||||
/// A diferencia del atributo `type` ([`form::input::Kind`]), no restringe los valores aceptados
|
||||
/// ni activa la validación del navegador; es sólo una sugerencia de presentación.
|
||||
#[builder_fn]
|
||||
pub fn with_inputmode(mut self, inputmode: Option<Mode>) -> Self {
|
||||
self.inputmode.alter_opt(inputmode);
|
||||
self
|
||||
}
|
||||
}
|
||||
484
extensions/pagetop-bootsier/src/theme/form/props.rs
Normal file
484
extensions/pagetop-bootsier/src/theme/form/props.rs
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
|
||||
// **< CheckboxKind >*******************************************************************************
|
||||
|
||||
/// Variante visual para [`form::Checkbox`](crate::theme::form::Checkbox) en un formulario.
|
||||
///
|
||||
/// Determina si el control se renderiza como una casilla de verificación estándar o como un
|
||||
/// interruptor (*toggle switch*).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum CheckboxKind {
|
||||
/// Casilla de verificación estándar. Es el tipo por defecto.
|
||||
#[default]
|
||||
Check,
|
||||
/// Interruptor de encendido/apagado.
|
||||
Switch,
|
||||
// TODO: Añadir variante `NativeSwitch` cuando el atributo `switch` de la propuesta WHATWG
|
||||
// (https://github.com/whatwg/html/issues/9546) sea estándar y tenga soporte amplio. Safari ya
|
||||
// lo soporta. También se añadiría el constructor `Checkbox::native_switch()`.
|
||||
}
|
||||
|
||||
// **< Autocomplete / AutofillField >***************************************************************
|
||||
|
||||
/// Configuración para el autocompletado de controles en un formulario.
|
||||
///
|
||||
/// Indica al navegador si puede sugerir o rellenar automáticamente el valor del control usando
|
||||
/// datos que el usuario haya introducido antes (credenciales guardadas, datos de contacto, etc.).
|
||||
///
|
||||
/// Lo habitual es usar uno de los **métodos predefinidos**, que generan el token canónico adecuado
|
||||
/// para cada tipo de dato:
|
||||
///
|
||||
/// - Identidad y credenciales: [`username()`](Autocomplete::username),
|
||||
/// [`email()`](Autocomplete::email), [`current_password()`](Autocomplete::current_password),
|
||||
/// [`new_password()`](Autocomplete::new_password), [`otp()`](Autocomplete::otp).
|
||||
/// - Token o tokens directos: [`token(field)`](Autocomplete::token) con una variante de
|
||||
/// [`AutofillField`].
|
||||
/// - Direcciones: [`shipping(field)`](Autocomplete::shipping),
|
||||
/// [`billing(field)`](Autocomplete::billing).
|
||||
/// - Datos de contacto: [`home(field)`](Autocomplete::home), [`work(field)`](Autocomplete::work),
|
||||
/// [`mobile(field)`](Autocomplete::mobile), [`fax(field)`](Autocomplete::fax),
|
||||
/// [`pager(field)`](Autocomplete::pager).
|
||||
/// - Sección personalizada: [`section(name, field)`](Autocomplete::section).
|
||||
///
|
||||
/// Para activar o inhibir el autocompletado sin especificar el tipo de dato basta con usar las
|
||||
/// variantes [`form::Autocomplete::On`](Autocomplete::On) o
|
||||
/// [`form::Autocomplete::Off`](Autocomplete::Off). Para combinaciones no cubiertas por los métodos
|
||||
/// anteriores, [`custom()`](Autocomplete::custom) acepta cualquier cadena ASCII válida.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// // Correo electrónico con sugerencia semántica del navegador.
|
||||
/// let ac = form::Autocomplete::email();
|
||||
///
|
||||
/// // Contraseña nueva en un formulario de registro.
|
||||
/// let ac = form::Autocomplete::new_password();
|
||||
///
|
||||
/// // Teléfono de contacto del trabajo.
|
||||
/// let ac = form::Autocomplete::work(form::AutofillField::Tel);
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Autocomplete {
|
||||
/// Genera `autocomplete="on"`.
|
||||
On,
|
||||
/// Genera `autocomplete="off"`.
|
||||
Off,
|
||||
/// Contiene el valor literal del atributo `autocomplete` tal como se enviará al navegador.
|
||||
///
|
||||
/// Debe contener un token o lista de tokens separados por espacios (p. ej. `"username"` o
|
||||
/// `"username webauthn"`).
|
||||
Custom(CowStr),
|
||||
}
|
||||
|
||||
impl Autocomplete {
|
||||
// --< Token >----------------------------------------------------------------------------------
|
||||
|
||||
/// Genera `autocomplete` a partir del token o tokens del [`AutofillField`] indicado.
|
||||
#[inline]
|
||||
pub fn token(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Borrowed(field.as_str()))
|
||||
}
|
||||
|
||||
// --< Secciones >------------------------------------------------------------------------------
|
||||
|
||||
/// Construye `autocomplete` con un prefijo de sección y un token o tokens del
|
||||
/// [`form::AutofillField`](AutofillField) indicado.
|
||||
///
|
||||
/// Genera `autocomplete="section-<name> <field>"`. Si `name` no es ASCII o contiene espacios,
|
||||
/// se ignora la sección y se genera sólo el token indicado.
|
||||
///
|
||||
/// El prefijo `section-*` sirve para distinguir entre varios grupos del mismo tipo en una misma
|
||||
/// página (p. ej. una dirección de envío y otra de facturación).
|
||||
pub fn section(name: impl AsRef<str>, field: AutofillField) -> Self {
|
||||
match util::normalize_ascii(name.as_ref()) {
|
||||
Ok(n) if !n.as_ref().contains(' ') => {
|
||||
Self::custom(util::join!("section-", n.as_ref(), " ", field.as_str()))
|
||||
}
|
||||
_ => Self::token(field),
|
||||
}
|
||||
}
|
||||
|
||||
// --< Comunes >--------------------------------------------------------------------------------
|
||||
|
||||
/// Genera `autocomplete="username"`.
|
||||
pub fn username() -> Self {
|
||||
Self::token(AutofillField::Username)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="username webauthn"` (Passkeys / WebAuthn).
|
||||
pub fn username_webauthn() -> Self {
|
||||
Self::custom("username webauthn")
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="email"`.
|
||||
pub fn email() -> Self {
|
||||
Self::token(AutofillField::Email)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="current-password"`.
|
||||
pub fn current_password() -> Self {
|
||||
Self::token(AutofillField::CurrentPassword)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="current-password webauthn"` (Passkeys / WebAuthn).
|
||||
pub fn current_password_webauthn() -> Self {
|
||||
Self::custom("current-password webauthn")
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="new-password"`.
|
||||
pub fn new_password() -> Self {
|
||||
Self::token(AutofillField::NewPassword)
|
||||
}
|
||||
|
||||
/// Genera `autocomplete="one-time-code"`.
|
||||
pub fn otp() -> Self {
|
||||
Self::token(AutofillField::OneTimeCode)
|
||||
}
|
||||
|
||||
// --< Direcciones >----------------------------------------------------------------------------
|
||||
|
||||
/// Contexto de dirección de envío. Genera `autocomplete="shipping <field>"`.
|
||||
pub fn shipping(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("shipping ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Contexto de dirección de facturación. Genera `autocomplete="billing <field>"`.
|
||||
pub fn billing(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("billing ", field.as_str())))
|
||||
}
|
||||
|
||||
// --< Contacto >-------------------------------------------------------------------------------
|
||||
|
||||
/// Detalle de contacto: `autocomplete="home <field>"`.
|
||||
pub fn home(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("home ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="work <field>"`.
|
||||
pub fn work(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("work ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="mobile <field>"`.
|
||||
pub fn mobile(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("mobile ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="fax <field>"`.
|
||||
pub fn fax(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("fax ", field.as_str())))
|
||||
}
|
||||
|
||||
/// Detalle de contacto: `autocomplete="pager <field>"`.
|
||||
pub fn pager(field: AutofillField) -> Self {
|
||||
Self::Custom(Cow::Owned(util::join!("pager ", field.as_str())))
|
||||
}
|
||||
|
||||
// --< Tokens personalizados >------------------------------------------------------------------
|
||||
|
||||
/// Crea un valor de `autocomplete` a partir de una cadena de texto libre.
|
||||
///
|
||||
/// Normaliza la entrada recortando espacios extra, compactando separadores y convirtiendo a
|
||||
/// minúsculas. Si el resultado es `"on"` u `"off"`, devuelve la variante correspondiente; si la
|
||||
/// entrada contiene caracteres no ASCII o queda vacía tras normalizar, devuelve
|
||||
/// [`form::Autocomplete::On`](Autocomplete::On).
|
||||
///
|
||||
/// Para los casos habituales se recomienda usar los métodos predefinidos de
|
||||
/// [`form::Autocomplete`](Autocomplete).
|
||||
pub fn custom(autocomplete: impl Into<CowStr>) -> Self {
|
||||
let value: CowStr = autocomplete.into();
|
||||
let raw = value.as_ref();
|
||||
|
||||
// Normaliza la entrada.
|
||||
let Some(normalized) = util::normalize_ascii_or_empty(raw, "Autocomplete::custom") else {
|
||||
return Self::On;
|
||||
};
|
||||
let autocomplete = normalized.as_ref();
|
||||
|
||||
// Identifica valores especiales.
|
||||
if autocomplete == "on" {
|
||||
return Self::On;
|
||||
}
|
||||
if autocomplete == "off" {
|
||||
return Self::Off;
|
||||
}
|
||||
|
||||
// Mantiene el `Cow` original si no cambia nada (no reserva espacio).
|
||||
if autocomplete == raw {
|
||||
return Self::Custom(value);
|
||||
}
|
||||
// En otro caso asigna espacio para la normalización.
|
||||
Self::Custom(Cow::Owned(normalized.into_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Autocomplete {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Autocomplete::On => f.write_str("on"),
|
||||
Autocomplete::Off => f.write_str("off"),
|
||||
Autocomplete::Custom(c) => f.write_str(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokens para el autocompletado de formularios con [`form::Autocomplete`](Autocomplete).
|
||||
///
|
||||
/// Representa los tokens de autorrelleno (*autofill field*) definidos por la
|
||||
/// [especificación WHATWG](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill-field)
|
||||
/// para el atributo `autocomplete`. Cada variante corresponde exactamente a un token canónico
|
||||
/// de dicha especificación.
|
||||
///
|
||||
/// Los valores se usan en combinación con [`form::Autocomplete`](Autocomplete) para construir el
|
||||
/// valor completo del atributo `autocomplete` de un control de formulario. Los métodos de
|
||||
/// [`form::Autocomplete`](Autocomplete) como [`token()`](Autocomplete::token),
|
||||
/// [`email()`](Autocomplete::email), [`shipping()`](Autocomplete::shipping) o
|
||||
/// [`section()`](Autocomplete::section) aceptan variantes de `AutofillField` para generar el token
|
||||
/// correspondiente.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let ac = form::Autocomplete::token(form::AutofillField::Username);
|
||||
/// let ac = form::Autocomplete::shipping(form::AutofillField::StreetAddress);
|
||||
/// let ac = form::Autocomplete::section("job", form::AutofillField::Email);
|
||||
/// ```
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum AutofillField {
|
||||
// --< Identidad / cuenta >---------------------------------------------------------------------
|
||||
/// Nombre completo.
|
||||
Name,
|
||||
/// Tratamiento o título (p. ej. "Sr.", "Sra.", "Dra.").
|
||||
HonorificPrefix,
|
||||
/// Nombre de pila.
|
||||
GivenName,
|
||||
/// Nombre adicional (p. ej. segundo nombre).
|
||||
AdditionalName,
|
||||
/// Apellidos.
|
||||
FamilyName,
|
||||
/// Sufijo honorífico (p. ej. "Jr.", "PhD").
|
||||
HonorificSuffix,
|
||||
/// Apodo.
|
||||
Nickname,
|
||||
/// Identificador de usuario (login).
|
||||
Username,
|
||||
|
||||
// --< Credenciales >---------------------------------------------------------------------------
|
||||
/// Contraseña actual.
|
||||
CurrentPassword,
|
||||
/// Nueva contraseña.
|
||||
NewPassword,
|
||||
/// Código de un solo uso (OTP).
|
||||
OneTimeCode,
|
||||
|
||||
// --< Organización >---------------------------------------------------------------------------
|
||||
/// Cargo o título dentro de una organización.
|
||||
OrganizationTitle,
|
||||
/// Nombre de la organización.
|
||||
Organization,
|
||||
|
||||
// --< Contacto >-------------------------------------------------------------------------------
|
||||
/// Correo electrónico.
|
||||
Email,
|
||||
/// Teléfono.
|
||||
Tel,
|
||||
/// Prefijo/código de país del teléfono (incluye `+`).
|
||||
TelCountryCode,
|
||||
/// Teléfono sin el código de país.
|
||||
TelNational,
|
||||
/// Código de área (si aplica).
|
||||
TelAreaCode,
|
||||
/// Teléfono sin código de país ni de área.
|
||||
TelLocal,
|
||||
/// Prefijo local (primera parte tras el área).
|
||||
TelLocalPrefix,
|
||||
/// Sufijo local (segunda parte tras el área).
|
||||
TelLocalSuffix,
|
||||
/// Extensión interna.
|
||||
TelExtension,
|
||||
/// URL personal o de contacto.
|
||||
Url,
|
||||
/// Referencia de mensajería instantánea (URL).
|
||||
Impp,
|
||||
|
||||
// --< Dirección >------------------------------------------------------------------------------
|
||||
/// Dirección postal completa (una sola línea/textarea).
|
||||
StreetAddress,
|
||||
/// Línea 1 de dirección.
|
||||
AddressLine1,
|
||||
/// Línea 2 de dirección.
|
||||
AddressLine2,
|
||||
/// Línea 3 de dirección.
|
||||
AddressLine3,
|
||||
/// Nivel administrativo 4 (el más específico).
|
||||
AddressLevel4,
|
||||
/// Nivel administrativo 3.
|
||||
AddressLevel3,
|
||||
/// Nivel administrativo 2 (p. ej. ciudad/municipio).
|
||||
AddressLevel2,
|
||||
/// Nivel administrativo 1 (p. ej. provincia/estado).
|
||||
AddressLevel1,
|
||||
/// Código postal.
|
||||
PostalCode,
|
||||
/// País (el navegador rellena el código de país).
|
||||
Country,
|
||||
/// Nombre del país.
|
||||
CountryName,
|
||||
|
||||
// --< Pago >-----------------------------------------------------------------------------------
|
||||
/// Nombre del titular de la tarjeta.
|
||||
CcName,
|
||||
/// Nombre de pila del titular de la tarjeta.
|
||||
CcGivenName,
|
||||
/// Nombre adicional del titular de la tarjeta.
|
||||
CcAdditionalName,
|
||||
/// Apellidos del titular de la tarjeta.
|
||||
CcFamilyName,
|
||||
/// Número de tarjeta.
|
||||
CcNumber,
|
||||
/// Fecha de caducidad (completa).
|
||||
CcExp,
|
||||
/// Mes de caducidad.
|
||||
CcExpMonth,
|
||||
/// Año de caducidad.
|
||||
CcExpYear,
|
||||
/// Código de seguridad (CVC/CVV).
|
||||
CcCsc,
|
||||
/// Tipo de tarjeta (p. ej. visa/mastercard).
|
||||
CcType,
|
||||
|
||||
// --< Transacción / preferencias >-------------------------------------------------------------
|
||||
/// Moneda preferida para la transacción (código ISO 4217).
|
||||
TransactionCurrency,
|
||||
/// Cantidad de la transacción (número).
|
||||
TransactionAmount,
|
||||
/// Idioma preferido (BCP 47).
|
||||
Language,
|
||||
|
||||
// --< Datos personales >-----------------------------------------------------------------------
|
||||
/// Fecha de nacimiento completa.
|
||||
Bday,
|
||||
/// Día de nacimiento.
|
||||
BdayDay,
|
||||
/// Mes de nacimiento.
|
||||
BdayMonth,
|
||||
/// Año de nacimiento.
|
||||
BdayYear,
|
||||
/// Sexo (valor libre guardado por el navegador).
|
||||
Sex,
|
||||
/// Foto (URL o referencia guardada por el navegador).
|
||||
Photo,
|
||||
}
|
||||
|
||||
impl AutofillField {
|
||||
/// Devuelve el token exacto definido por HTML para `autocomplete`.
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
AutofillField::Name => "name",
|
||||
AutofillField::HonorificPrefix => "honorific-prefix",
|
||||
AutofillField::GivenName => "given-name",
|
||||
AutofillField::AdditionalName => "additional-name",
|
||||
AutofillField::FamilyName => "family-name",
|
||||
AutofillField::HonorificSuffix => "honorific-suffix",
|
||||
AutofillField::Nickname => "nickname",
|
||||
AutofillField::Username => "username",
|
||||
|
||||
AutofillField::CurrentPassword => "current-password",
|
||||
AutofillField::NewPassword => "new-password",
|
||||
AutofillField::OneTimeCode => "one-time-code",
|
||||
|
||||
AutofillField::OrganizationTitle => "organization-title",
|
||||
AutofillField::Organization => "organization",
|
||||
|
||||
AutofillField::Email => "email",
|
||||
AutofillField::Tel => "tel",
|
||||
AutofillField::TelCountryCode => "tel-country-code",
|
||||
AutofillField::TelNational => "tel-national",
|
||||
AutofillField::TelAreaCode => "tel-area-code",
|
||||
AutofillField::TelLocal => "tel-local",
|
||||
AutofillField::TelLocalPrefix => "tel-local-prefix",
|
||||
AutofillField::TelLocalSuffix => "tel-local-suffix",
|
||||
AutofillField::TelExtension => "tel-extension",
|
||||
AutofillField::Url => "url",
|
||||
AutofillField::Impp => "impp",
|
||||
|
||||
AutofillField::StreetAddress => "street-address",
|
||||
AutofillField::AddressLine1 => "address-line1",
|
||||
AutofillField::AddressLine2 => "address-line2",
|
||||
AutofillField::AddressLine3 => "address-line3",
|
||||
AutofillField::AddressLevel4 => "address-level4",
|
||||
AutofillField::AddressLevel3 => "address-level3",
|
||||
AutofillField::AddressLevel2 => "address-level2",
|
||||
AutofillField::AddressLevel1 => "address-level1",
|
||||
AutofillField::PostalCode => "postal-code",
|
||||
AutofillField::Country => "country",
|
||||
AutofillField::CountryName => "country-name",
|
||||
|
||||
AutofillField::CcName => "cc-name",
|
||||
AutofillField::CcGivenName => "cc-given-name",
|
||||
AutofillField::CcAdditionalName => "cc-additional-name",
|
||||
AutofillField::CcFamilyName => "cc-family-name",
|
||||
AutofillField::CcNumber => "cc-number",
|
||||
AutofillField::CcExp => "cc-exp",
|
||||
AutofillField::CcExpMonth => "cc-exp-month",
|
||||
AutofillField::CcExpYear => "cc-exp-year",
|
||||
AutofillField::CcCsc => "cc-csc",
|
||||
AutofillField::CcType => "cc-type",
|
||||
|
||||
AutofillField::TransactionCurrency => "transaction-currency",
|
||||
AutofillField::TransactionAmount => "transaction-amount",
|
||||
AutofillField::Language => "language",
|
||||
|
||||
AutofillField::Bday => "bday",
|
||||
AutofillField::BdayDay => "bday-day",
|
||||
AutofillField::BdayMonth => "bday-month",
|
||||
AutofillField::BdayYear => "bday-year",
|
||||
AutofillField::Sex => "sex",
|
||||
AutofillField::Photo => "photo",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< Method >*************************************************************************************
|
||||
|
||||
/// Método HTTP usado por un formulario ([`Form`](crate::theme::Form)) para el envío de los datos.
|
||||
///
|
||||
/// En HTML, el atributo `method` del formulario indica **cómo** se envían los datos:
|
||||
///
|
||||
/// - **GET**: los pares `name=value` se codifican en la **URL** añadiendo una cadena de consulta
|
||||
/// como `?a=1&b=2`. Es el método por defecto en HTML cuando no se especifica. Suele ser apropiado
|
||||
/// para **búsquedas** o formularios que no modifican datos ni el estado del sistema.
|
||||
///
|
||||
/// - **POST**: los datos se envían en el **cuerpo** de la petición (*request body*). Es apropiado
|
||||
/// para acciones que **modifican el estado** o cuando hay formularios grandes. Es el **método por
|
||||
/// defecto** en PageTop.
|
||||
///
|
||||
/// # Consideraciones prácticas
|
||||
///
|
||||
/// - **Visibilidad y privacidad**: con GET los datos quedan visibles en la URL (historial, *logs*,
|
||||
/// marcadores). No se recomienda para datos sensibles. Con POST no van en la URL, pero **no se
|
||||
/// cifran** por sí mismos; por eso es esencial el uso de HTTPS.
|
||||
/// - **Tamaño**: GET está limitado por la longitud máxima de URL que acepten el navegador y el
|
||||
/// servidor. POST es más flexible para cargas grandes.
|
||||
/// - **Ficheros**: la subida de ficheros requiere `method="post"` y un `enctype` adecuado
|
||||
/// (habitualmente `multipart/form-data`).
|
||||
#[derive(AutoDefault, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Method {
|
||||
/// Envía los datos en el cuerpo de la petición.
|
||||
///
|
||||
/// Es el **método por defecto** en PageTop. Recomendado para operaciones que modifican el
|
||||
/// estado o para envíos grandes.
|
||||
#[default]
|
||||
Post,
|
||||
|
||||
/// Envía los datos en la URL como una cadena *query*.
|
||||
///
|
||||
/// Recomendado para búsquedas y operaciones que no modifican datos ni el estado del sistema.
|
||||
Get,
|
||||
}
|
||||
269
extensions/pagetop-bootsier/src/theme/form/radio.rs
Normal file
269
extensions/pagetop-bootsier/src/theme/form/radio.rs
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
//! Definiciones para crear grupos de botones de opción (*radio buttons*).
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Botón de opción individual de un [`form::radio::Field`](Field).
|
||||
///
|
||||
/// Representa cada opción de un grupo de opciones exclusivas entre sí, con un valor (el que se
|
||||
/// envía al servidor), una etiqueta localizable visible y puede marcarse como seleccionada o
|
||||
/// inicialmente deshabilitada de forma independiente.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let item = form::radio::Item::new("monthly", L10n::n("Monthly")).with_checked(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
/// Devuelve el valor enviado al servidor cuando la opción está seleccionada.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta de la opción.
|
||||
label: L10n,
|
||||
/// Devuelve si la opción debe aparecer seleccionada por defecto.
|
||||
checked: bool,
|
||||
/// Devuelve si la opción está deshabilitada.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea una nueva opción con el valor y la etiqueta indicados.
|
||||
pub fn new(value: impl AsRef<str>, label: L10n) -> Self {
|
||||
Self {
|
||||
value: AttrValue::new(value),
|
||||
label,
|
||||
checked: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// Establece si la opción aparece seleccionada por defecto.
|
||||
///
|
||||
/// Si varias opciones del grupo tienen `checked` activo, sólo la primera se renderizará como
|
||||
/// seleccionada; las demás se ignorarán.
|
||||
pub fn with_checked(mut self, checked: bool) -> Self {
|
||||
self.checked = checked;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la opción está inicialmente deshabilitada.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// **< Field >**************************************************************************************
|
||||
|
||||
/// Componente para crear un **grupo de botones de opción**.
|
||||
///
|
||||
/// Renderiza un grupo de botones de opción [`form::radio::Item`](Item) que comparten el mismo
|
||||
/// atributo `name`, por lo que sólo puede seleccionarse uno a la vez. Las opciones se añaden con
|
||||
/// [`with_item()`](Field::with_item).
|
||||
///
|
||||
/// Si se activa el modo en línea [`with_inline()`](Field::with_inline), los botones se disponen
|
||||
/// horizontalmente. El atributo `required` se propaga a todos los botones del grupo para cumplir
|
||||
/// con la especificación HTML.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let plan = form::radio::Field::new()
|
||||
/// .with_name("plan")
|
||||
/// .with_label(L10n::n("Subscription plan"))
|
||||
/// .with_item(form::radio::Item::new("monthly", L10n::n("Monthly")))
|
||||
/// .with_item(form::radio::Item::new("annual", L10n::n("Annual")).with_checked(true))
|
||||
/// .with_required(true);
|
||||
/// ```
|
||||
///
|
||||
/// Cuando el usuario selecciona un botón, el navegador envía algo como `plan=monthly`; si no
|
||||
/// selecciona ninguno, no envía nada. En el servidor el campo se deserializa como `Option<String>`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// plan: Option<String>, // Some("monthly"), Some("annual"), ..., o None si no se seleccionó.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Field {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del grupo.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre compartido por todos los botones de opción del grupo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del grupo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve el texto de ayuda del grupo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve las opciones del grupo.
|
||||
items: Vec<Item>,
|
||||
/// Devuelve si la selección de alguna opción del grupo es obligatoria.
|
||||
required: bool,
|
||||
/// Devuelve si todo el grupo está deshabilitado.
|
||||
disabled: bool,
|
||||
/// Devuelve si los botones se muestran en línea horizontalmente.
|
||||
inline: bool,
|
||||
}
|
||||
|
||||
impl Component for Field {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-radios");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let name = self
|
||||
.name()
|
||||
.get()
|
||||
.unwrap_or_else(|| cx.required_id::<Self>(self.id(), 3));
|
||||
let container_id = self.id().unwrap_or_else(|| util::join!("edit-", &name));
|
||||
Ok(html! {
|
||||
div id=(&container_id) class=[self.classes().get()] {
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label class="form-label" {
|
||||
(label)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@let item_classes = if *self.inline() {
|
||||
"form-check form-check-inline"
|
||||
} else {
|
||||
"form-check"
|
||||
};
|
||||
@let mut do_check = true;
|
||||
@for (item, i) in self.items().iter().zip(1..) {
|
||||
@let checked = {
|
||||
let c = *item.checked() && do_check;
|
||||
if c { do_check = false; }
|
||||
c
|
||||
};
|
||||
@let i = i.to_string();
|
||||
@let item_id = util::join!(&container_id, "-radio-", &i);
|
||||
div class=(item_classes) {
|
||||
input
|
||||
type="radio"
|
||||
id=(&item_id)
|
||||
class="form-check-input"
|
||||
name=(&name)
|
||||
value=[item.value().get()]
|
||||
checked[checked]
|
||||
required[*self.required()]
|
||||
disabled[*item.disabled() || *self.disabled()];
|
||||
label class="form-check-label" for=(&item_id) {
|
||||
(item.label().using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
// **< Field BUILDER >**************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del grupo de opciones.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del grupo de opciones.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre compartido por todos los botones de opción del grupo.
|
||||
///
|
||||
/// Todas las opciones [`form::radio::Item`](Item) del grupo llevarán este mismo `name`, lo que
|
||||
/// garantiza la exclusividad de la selección. Es imprescindible establecer un `name`; sin él
|
||||
/// los botones no se envían al servidor.
|
||||
///
|
||||
/// Si se omite, se asigna un nombre generado automáticamente. Para deserializar los campos en
|
||||
/// el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del grupo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del grupo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade una opción al grupo. Las opciones se muestran en el orden en que se añaden.
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, item: Item) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la selección de alguna opción del grupo es obligatoria.
|
||||
///
|
||||
/// El atributo `required` se propaga a todos los botones del grupo para cumplir con la
|
||||
/// especificación HTML.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si todo el grupo está deshabilitado.
|
||||
///
|
||||
/// Cuando está activo, se combina con el estado `disabled` de cada [`Item`].
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si los botones se muestran en línea horizontalmente.
|
||||
///
|
||||
/// Al activar este modo, se añade la clase `form-check-inline` al contenedor de cada opción.
|
||||
#[builder_fn]
|
||||
pub fn with_inline(mut self, inline: bool) -> Self {
|
||||
self.inline = inline;
|
||||
self
|
||||
}
|
||||
}
|
||||
192
extensions/pagetop-bootsier/src/theme/form/range.rs
Normal file
192
extensions/pagetop-bootsier/src/theme/form/range.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
/// Componente para crear un **control deslizante** de rango.
|
||||
///
|
||||
/// Renderiza una barra deslizante con una etiqueta opcional y un texto de ayuda. Permite
|
||||
/// seleccionar un valor de entre una lista de valores posibles, acotados por un valor mínimo y
|
||||
/// máximo, con un paso opcional entre valores.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let volume = form::Range::new()
|
||||
/// .with_name("volume")
|
||||
/// .with_label(L10n::n("Volume"))
|
||||
/// .with_min(Some(0.0))
|
||||
/// .with_max(Some(100.0))
|
||||
/// .with_step(Some(5.0))
|
||||
/// .with_value(Some(50.0));
|
||||
/// ```
|
||||
///
|
||||
/// Al enviar el formulario el navegador transmite `name=valor`. Un control deslizante siempre
|
||||
/// envía su valor. En el servidor se deserializa como `f64`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// volume: f64, // Siempre presente con el valor numérico seleccionado.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Range {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del control deslizante.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del campo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve el texto de ayuda del campo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve el valor mínimo permitido.
|
||||
min: Attr<f64>,
|
||||
/// Devuelve el valor máximo permitido.
|
||||
max: Attr<f64>,
|
||||
/// Devuelve el incremento entre valores del campo.
|
||||
step: Attr<f64>,
|
||||
/// Devuelve el valor inicial del campo.
|
||||
value: Attr<f64>,
|
||||
/// Devuelve si el control recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el control está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Component for Range {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-range");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let container_id = self
|
||||
.id()
|
||||
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
||||
let range_id = container_id.as_deref().map(|id| util::join!(id, "-range"));
|
||||
Ok(html! {
|
||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||
@if let Some(label) = self.label().lookup(cx) {
|
||||
label for=[range_id.as_deref()] class="form-label" { (label) }
|
||||
}
|
||||
input
|
||||
type="range"
|
||||
id=[range_id.as_deref()]
|
||||
class="form-range"
|
||||
name=[self.name().get()]
|
||||
min=[self.min().get()]
|
||||
max=[self.max().get()]
|
||||
step=[self.step().get()]
|
||||
value=[self.value().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
disabled[*self.disabled()];
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Range {
|
||||
// **< Range BUILDER >**************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del contenedor del control deslizante.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del control deslizante.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor mínimo del rango.
|
||||
///
|
||||
/// Pasar `None` omite el atributo `min` y deja que el navegador aplique su valor por defecto.
|
||||
#[builder_fn]
|
||||
pub fn with_min(mut self, min: Option<f64>) -> Self {
|
||||
self.min.alter_opt(min);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor máximo del rango.
|
||||
///
|
||||
/// Pasar `None` omite el atributo `max` y deja que el navegador aplique su valor por defecto.
|
||||
#[builder_fn]
|
||||
pub fn with_max(mut self, max: Option<f64>) -> Self {
|
||||
self.max.alter_opt(max);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el incremento entre valores del campo.
|
||||
///
|
||||
/// Pasar `None` omite el atributo `step` y deja que el navegador aplique su valor por defecto
|
||||
/// (normalmente `1`).
|
||||
#[builder_fn]
|
||||
pub fn with_step(mut self, step: Option<f64>) -> Self {
|
||||
self.step.alter_opt(step);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor inicial del campo.
|
||||
///
|
||||
/// Pasar `None` omite el atributo `value` y deja que el navegador aplique su valor por defecto
|
||||
/// (normalmente el punto medio del rango).
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: Option<f64>) -> Self {
|
||||
self.value.alter_opt(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
459
extensions/pagetop-bootsier/src/theme/form/select.rs
Normal file
459
extensions/pagetop-bootsier/src/theme/form/select.rs
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
//! Definiciones para crear listas de selección.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
// **< Item >***************************************************************************************
|
||||
|
||||
/// Elemento individual de [`form::select::Field`] o de [`form::select::Group`].
|
||||
///
|
||||
/// Representa un elemento dentro de una lista de selección o de un grupo de elementos de la lista.
|
||||
/// Cada elemento tiene un valor que se envía al servidor y una etiqueta localizable visible para el
|
||||
/// usuario.
|
||||
///
|
||||
/// Puede marcarse como seleccionado por defecto con [`with_selected()`](Self::with_selected) o
|
||||
/// deshabilitado de forma independiente al resto usando [`with_disabled()`](Self::with_disabled).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let item = form::select::Item::new("es", L10n::n("Spanish")).with_selected(true);
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
/// Devuelve el valor enviado al servidor cuando se selecciona el elemento.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta visible del elemento.
|
||||
label: L10n,
|
||||
/// Devuelve si el elemento debe aparecer seleccionado por defecto.
|
||||
selected: bool,
|
||||
/// Devuelve si el elemento está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un nuevo elemento con el valor y la etiqueta indicados.
|
||||
pub fn new(value: impl AsRef<str>, label: L10n) -> Self {
|
||||
Self {
|
||||
value: AttrValue::new(value),
|
||||
label,
|
||||
selected: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// **< Item BUILDER >***************************************************************************
|
||||
|
||||
/// Establece si el elemento aparece seleccionado por defecto.
|
||||
///
|
||||
/// En una lista de selección única, el navegador aplica la selección al último elemento marcado
|
||||
/// si hay más de uno; mientras que en una lista múltiple se respetan todos los elementos
|
||||
/// marcados.
|
||||
pub fn with_selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el elemento está deshabilitado.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// **< Group >**************************************************************************************
|
||||
|
||||
/// Grupo de elementos dentro de [`form::select::Field`].
|
||||
///
|
||||
/// Agrupa un conjunto de elementos dentro de una lista de selección con una etiqueta visible. El
|
||||
/// grupo completo puede deshabilitarse en bloque con [`with_disabled()`](Self::with_disabled).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let group = form::select::Group::new(L10n::n("Europe"))
|
||||
/// .with_item(form::select::Item::new("es", L10n::n("Spanish")))
|
||||
/// .with_item(form::select::Item::new("fr", L10n::n("French")));
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Group {
|
||||
/// Devuelve la etiqueta visible del grupo de elementos.
|
||||
label: L10n,
|
||||
/// Devuelve los elementos del grupo.
|
||||
items: Vec<Item>,
|
||||
/// Devuelve si el grupo de elementos está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
/// Crea un nuevo grupo con la etiqueta indicada.
|
||||
pub fn new(label: L10n) -> Self {
|
||||
Self {
|
||||
label,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
// **< Group BUILDER >**************************************************************************
|
||||
|
||||
/// Añade un elemento al grupo. Los elementos se muestran en el orden en que se añaden.
|
||||
pub fn with_item(mut self, item: Item) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el grupo de elementos está deshabilitado en bloque.
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// **< Entry >**************************************************************************************
|
||||
|
||||
/// Entrada de [`form::select::Field`] con un elemento o un grupo de elementos.
|
||||
///
|
||||
/// Cada entrada se crea implícitamente cuando se usa [`form::select::Field::with_item()`] para
|
||||
/// añadir un elemento individual o [`form::select::Field::with_group()`] para añadir un grupo de
|
||||
/// elementos a una lista de selección.
|
||||
///
|
||||
/// Con [`form::select::Field::entries()`] se pueden recuperar todas las entradas para su
|
||||
/// renderizado.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Entry {
|
||||
/// Elemento individual.
|
||||
Item(Item),
|
||||
/// Grupo de elementos.
|
||||
Group(Group),
|
||||
}
|
||||
|
||||
// **< Field >**************************************************************************************
|
||||
|
||||
/// Componente para crear una **lista de selección**.
|
||||
///
|
||||
/// Renderiza un campo para mostrar una lista de elementos con una etiqueta opcional. Permite elegir
|
||||
/// uno, o más de uno si se activa la selección múltiple con
|
||||
/// [`with_multiple()`](Self::with_multiple).
|
||||
///
|
||||
/// Los elementos individuales se añaden con [`with_item()`](Self::with_item); los grupos de
|
||||
/// elementos con un encabezado común se añaden con [`with_group()`](Self::with_group). Ambos
|
||||
/// métodos pueden combinarse libremente.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let idioma = form::select::Field::new()
|
||||
/// .with_name("language")
|
||||
/// .with_label(L10n::n("Language"))
|
||||
/// .with_item(form::select::Item::new("", L10n::n("— Choose —")).with_selected(true))
|
||||
/// .with_group(
|
||||
/// form::select::Group::new(L10n::n("Europe"))
|
||||
/// .with_item(form::select::Item::new("es", L10n::n("Spanish")))
|
||||
/// .with_item(form::select::Item::new("fr", L10n::n("French"))),
|
||||
/// )
|
||||
/// .with_group(
|
||||
/// form::select::Group::new(L10n::n("Americas"))
|
||||
/// .with_item(form::select::Item::new("en", L10n::n("English")))
|
||||
/// .with_item(form::select::Item::new("pt", L10n::n("Portuguese"))),
|
||||
/// )
|
||||
/// .with_required(true);
|
||||
/// ```
|
||||
///
|
||||
/// Cuando el usuario selecciona un elemento y envía el formulario, el navegador transmite
|
||||
/// `name=valor`. Si el campo es obligatorio el valor siempre estará presente y puede deserializarse
|
||||
/// como `String`; si es opcional, usa `Option<String>`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// language: String, // Siempre presente (campo obligatorio).
|
||||
/// // language: Option<String>, // None si no se selecciona ninguna opción.
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Con selección múltiple activa, el navegador envía un valor por cada elemento marcado; si no se
|
||||
/// marca ninguno, no envía nada. Usa `Vec<String>` con `#[serde(default)]`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// #[serde(default)]
|
||||
/// interests: Vec<String>, // p. ej. ["art", "tech"] o [] si no se marcó ninguna.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Field {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor de la lista de selección.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve la etiqueta del campo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si la etiqueta se muestra flotante sobre el campo.
|
||||
floating_label: bool,
|
||||
/// Devuelve el texto de ayuda del campo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve las entradas de la lista (elementos individuales y grupos de elementos).
|
||||
entries: Vec<Entry>,
|
||||
/// Devuelve si la lista permite selección múltiple.
|
||||
multiple: bool,
|
||||
/// Devuelve el número de filas visibles de la lista de selección.
|
||||
rows: Attr<u16>,
|
||||
/// Devuelve la configuración de autocompletado del campo.
|
||||
autocomplete: Attr<form::Autocomplete>,
|
||||
/// Devuelve si la lista recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si la selección de un elemento es obligatoria.
|
||||
required: bool,
|
||||
/// Devuelve si la lista está deshabilitada.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Component for Field {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if *self.floating_label() {
|
||||
self.multiple = false;
|
||||
self.rows.alter_opt(None::<u16>);
|
||||
self.alter_classes(ClassesOp::Prepend, "form-floating");
|
||||
}
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-select");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let container_id = self
|
||||
.id()
|
||||
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
||||
let select_id = container_id.as_deref().map(|id| util::join!(id, "-select"));
|
||||
let label = match self.label().lookup(cx) {
|
||||
Some(text) => html! {
|
||||
label for=[select_id.as_deref()] class="form-label" {
|
||||
(text)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => html! {},
|
||||
};
|
||||
Ok(html! {
|
||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||
@if !*self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
select
|
||||
id=[select_id.as_deref()]
|
||||
class="form-select"
|
||||
name=[self.name().get()]
|
||||
multiple[*self.multiple()]
|
||||
size=[self.rows().get()]
|
||||
autocomplete=[self.autocomplete().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
required[*self.required()]
|
||||
disabled[*self.disabled()]
|
||||
{
|
||||
@for entry in self.entries() {
|
||||
@match entry {
|
||||
Entry::Item(opt) => {
|
||||
option
|
||||
value=(opt.value().as_str().unwrap_or(""))
|
||||
selected[*opt.selected()]
|
||||
disabled[*opt.disabled()]
|
||||
{
|
||||
(opt.label().using(cx))
|
||||
}
|
||||
}
|
||||
Entry::Group(group) => {
|
||||
optgroup
|
||||
label=(group.label().using(cx))
|
||||
disabled[*group.disabled()]
|
||||
{
|
||||
@for opt in group.items() {
|
||||
option
|
||||
value=(opt.value().as_str().unwrap_or(""))
|
||||
selected[*opt.selected()]
|
||||
disabled[*opt.disabled()]
|
||||
{
|
||||
(opt.label().using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if *self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Field {
|
||||
// **< Field BUILDER >***************************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del contenedor del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor de la lista de selección.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor seleccionado no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la etiqueta se muestra flotante sobre el campo.
|
||||
///
|
||||
/// Cuando está activo, la etiqueta se superpone al control y permanece flotante siempre que
|
||||
/// haya una opción visible.
|
||||
///
|
||||
/// Si se usa la etiqueta flotante, el [`setup()`](Self::setup) del componente anulará los
|
||||
/// valores establecidos con [`with_multiple()`](Self::with_multiple) y
|
||||
/// [`with_rows()`](Self::with_rows) antes del renderizado.
|
||||
#[builder_fn]
|
||||
pub fn with_floating_label(mut self, floating_label: bool) -> Self {
|
||||
self.floating_label = floating_label;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un elemento individual a la lista de selección.
|
||||
///
|
||||
/// Los elementos y grupos se muestran en el orden en que se añaden.
|
||||
#[builder_fn]
|
||||
pub fn with_item(mut self, item: Item) -> Self {
|
||||
self.entries.push(Entry::Item(item));
|
||||
self
|
||||
}
|
||||
|
||||
/// Añade un grupo de elementos a la lista de selección.
|
||||
///
|
||||
/// Los elementos y grupos se muestran en el orden en que se añaden.
|
||||
#[builder_fn]
|
||||
pub fn with_group(mut self, group: Group) -> Self {
|
||||
self.entries.push(Entry::Group(group));
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el control permite seleccionar varios elementos.
|
||||
///
|
||||
/// Al activar la selección múltiple, se muestra una lista en lugar de un desplegable. Se
|
||||
/// recomienda combinar con [`with_rows()`](Self::with_rows) para controlar el número de filas
|
||||
/// visibles.
|
||||
///
|
||||
/// Para un número reducido de elementos con etiquetas descriptivas considera usar
|
||||
/// [`form::check::Field`] en su lugar, ofrece una presentación más clara y es más accesible en
|
||||
/// pantallas pequeñas.
|
||||
///
|
||||
/// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label).
|
||||
#[builder_fn]
|
||||
pub fn with_multiple(mut self, multiple: bool) -> Self {
|
||||
self.multiple = multiple;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el número de filas visibles de la lista de selección.
|
||||
///
|
||||
/// Cuando se establece un valor mayor que 1, el control se muestra como lista en lugar de
|
||||
/// desplegable, tanto en modo simple como múltiple. Con `None` se omite el atributo y presenta
|
||||
/// el control como desplegable (comportamiento por defecto).
|
||||
///
|
||||
/// Es especialmente útil con selección múltiple para controlar el número de filas visibles sin
|
||||
/// necesidad de recurrir al desplazamiento.
|
||||
///
|
||||
/// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label).
|
||||
#[builder_fn]
|
||||
pub fn with_rows(mut self, rows: Option<u16>) -> Self {
|
||||
self.rows.alter_opt(rows);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la configuración de autocompletado del campo.
|
||||
///
|
||||
/// Permite al navegador rellenar automáticamente el elemento seleccionado en listas de países
|
||||
/// (`"country"`), idiomas (`"language"`), sexo (`"sex"`) u otros campos con valores
|
||||
/// predefinidos. En listas de selección múltiples no es útil en la práctica, ya que los
|
||||
/// navegadores no gestionan selecciones múltiples con autocompletado.
|
||||
///
|
||||
/// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para
|
||||
/// omitir el atributo.
|
||||
#[builder_fn]
|
||||
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self {
|
||||
self.autocomplete.alter_opt(autocomplete);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
290
extensions/pagetop-bootsier/src/theme/form/textarea.rs
Normal file
290
extensions/pagetop-bootsier/src/theme/form/textarea.rs
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
use pagetop::prelude::*;
|
||||
|
||||
use crate::theme::form;
|
||||
use crate::LOCALES_BOOTSIER;
|
||||
|
||||
/// Componente para crear un **área de texto** de formulario.
|
||||
///
|
||||
/// Permite escribir en un área de texto de más de una línea, con una etiqueta opcional y atributos
|
||||
/// como el número de filas a presentar, longitud mínima (`minlength`) y máxima (`maxlength`), texto
|
||||
/// indicativo (`placeholder`) o autocompletado (`autocomplete`).
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop::prelude::*;
|
||||
/// # use pagetop_bootsier::prelude::*;
|
||||
/// let descripcion = form::Textarea::new()
|
||||
/// .with_name("description")
|
||||
/// .with_label(L10n::n("Description"))
|
||||
/// .with_rows(Some(8))
|
||||
/// .with_maxlength(Some(500))
|
||||
/// .with_placeholder(L10n::n("Write here..."))
|
||||
/// .with_required(true);
|
||||
/// ```
|
||||
///
|
||||
/// Al enviar el formulario el navegador transmite `name=valor`. Un área de texto siempre envía su
|
||||
/// valor, incluso si está vacía. En el servidor se deserializa como `String`:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[derive(serde::Deserialize)]
|
||||
/// struct FormData {
|
||||
/// description: String, // Siempre presente; cadena vacía si el usuario no escribió nada.
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Textarea {
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS del contenedor del área de texto.
|
||||
classes: Classes,
|
||||
/// Devuelve el nombre del campo.
|
||||
name: AttrName,
|
||||
/// Devuelve el valor inicial del área de texto.
|
||||
value: AttrValue,
|
||||
/// Devuelve la etiqueta del campo.
|
||||
label: Attr<L10n>,
|
||||
/// Devuelve si la etiqueta se muestra flotante sobre el campo.
|
||||
floating_label: bool,
|
||||
/// Devuelve el texto de ayuda del campo.
|
||||
help_text: Attr<L10n>,
|
||||
/// Devuelve el número de filas visibles del área de texto.
|
||||
rows: Attr<u16>,
|
||||
/// Devuelve la longitud mínima permitida en caracteres.
|
||||
minlength: Attr<u16>,
|
||||
/// Devuelve la longitud máxima permitida en caracteres.
|
||||
maxlength: Attr<u16>,
|
||||
/// Devuelve el texto indicativo del área de texto.
|
||||
placeholder: Attr<L10n>,
|
||||
/// Devuelve la configuración de autocompletado del campo.
|
||||
autocomplete: Attr<form::Autocomplete>,
|
||||
/// Devuelve si el campo recibe el foco automáticamente al cargar la página.
|
||||
autofocus: bool,
|
||||
/// Devuelve si el campo es de sólo lectura.
|
||||
readonly: bool,
|
||||
/// Devuelve si el campo es obligatorio.
|
||||
required: bool,
|
||||
/// Devuelve si el campo está deshabilitado.
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl Component for Textarea {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if *self.floating_label() {
|
||||
self.rows.alter_opt(None::<u16>);
|
||||
self.alter_classes(ClassesOp::Prepend, "form-floating");
|
||||
}
|
||||
self.alter_classes(ClassesOp::Prepend, "form-field form-field-textarea");
|
||||
}
|
||||
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let container_id = self
|
||||
.id()
|
||||
.or_else(|| self.name().get().map(|n| util::join!("edit-", n)));
|
||||
let textarea_id = container_id
|
||||
.as_deref()
|
||||
.map(|id| util::join!(id, "-textarea"));
|
||||
// La etiqueta flotante requiere el atributo `placeholder` para detectar cuándo el campo
|
||||
// está vacío y animar la etiqueta; si no está definido, se fuerza `placeholder=""`.
|
||||
let placeholder = if *self.floating_label() {
|
||||
Some(self.placeholder().lookup(cx).unwrap_or_default())
|
||||
} else {
|
||||
self.placeholder().lookup(cx)
|
||||
};
|
||||
let label = match self.label().lookup(cx) {
|
||||
Some(text) => html! {
|
||||
label for=[textarea_id.as_deref()] class="form-label" {
|
||||
(text)
|
||||
@if *self.required() {
|
||||
span
|
||||
class="form-required"
|
||||
title=(L10n::t("input_required", &LOCALES_BOOTSIER).using(cx))
|
||||
{
|
||||
"*"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => html! {},
|
||||
};
|
||||
Ok(html! {
|
||||
div id=[container_id.as_deref()] class=[self.classes().get()] {
|
||||
@if !*self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
textarea
|
||||
id=[textarea_id.as_deref()]
|
||||
class="form-control"
|
||||
name=[self.name().get()]
|
||||
rows=[self.rows().get()]
|
||||
minlength=[self.minlength().get()]
|
||||
maxlength=[self.maxlength().get()]
|
||||
placeholder=[placeholder]
|
||||
autocomplete=[self.autocomplete().get()]
|
||||
autofocus[*self.autofocus()]
|
||||
readonly[*self.readonly()]
|
||||
required[*self.required()]
|
||||
disabled[*self.disabled()]
|
||||
{
|
||||
@if let Some(value) = self.value().get() {
|
||||
(value)
|
||||
}
|
||||
}
|
||||
@if *self.floating_label() {
|
||||
(label)
|
||||
}
|
||||
@if let Some(description) = self.help_text().lookup(cx) {
|
||||
div class="form-text" { (description) }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Textarea {
|
||||
// **< Textarea BUILDER >***********************************************************************
|
||||
|
||||
/// Establece el identificador único (`id`) del contenedor del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al contenedor del campo.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el nombre del campo (atributo `name`).
|
||||
///
|
||||
/// Sin él, el valor del campo no se transmite al servidor al enviar el formulario. Para
|
||||
/// deserializar el campo en el servidor es recomendable establecer un `name` explícito.
|
||||
#[builder_fn]
|
||||
pub fn with_name(mut self, name: impl AsRef<str>) -> Self {
|
||||
self.name.alter_name(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el valor inicial del área de texto.
|
||||
#[builder_fn]
|
||||
pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
|
||||
self.value.alter_str(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina la etiqueta visible del campo (basta pasar `None` para quitarla).
|
||||
#[builder_fn]
|
||||
pub fn with_label(mut self, label: impl Into<Option<L10n>>) -> Self {
|
||||
self.label.alter_opt(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si la etiqueta se muestra flotante sobre el campo.
|
||||
///
|
||||
/// Cuando está activo, la etiqueta se superpone al área de texto y asciende al enfocarlo o
|
||||
/// cuando tiene contenido.
|
||||
///
|
||||
/// Si se usa la etiqueta flotante, el [`setup()`](Self::setup) del componente anulará el valor
|
||||
/// establecido con [`with_rows()`](Self::with_rows) antes del renderizado. Si es necesario, se
|
||||
/// puede controlar la altura con estilos aplicados al componente.
|
||||
#[builder_fn]
|
||||
pub fn with_floating_label(mut self, floating_label: bool) -> Self {
|
||||
self.floating_label = floating_label;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto de ayuda del campo (basta pasar `None` para quitarlo).
|
||||
#[builder_fn]
|
||||
pub fn with_help_text(mut self, help_text: impl Into<Option<L10n>>) -> Self {
|
||||
self.help_text.alter_opt(help_text.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece el número de filas visibles del área de texto.
|
||||
///
|
||||
/// Sin valor o pasando `None`, el área muestra su altura predeterminada, dos filas según el
|
||||
/// estándar.
|
||||
///
|
||||
/// Se anula si se usa con [`with_floating_label(true)`](Self::with_floating_label).
|
||||
#[builder_fn]
|
||||
pub fn with_rows(mut self, rows: Option<u16>) -> Self {
|
||||
self.rows.alter_opt(rows);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la longitud mínima permitida en caracteres.
|
||||
#[builder_fn]
|
||||
pub fn with_minlength(mut self, minlength: Option<u16>) -> Self {
|
||||
self.minlength.alter_opt(minlength);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la longitud máxima permitida en caracteres.
|
||||
#[builder_fn]
|
||||
pub fn with_maxlength(mut self, maxlength: Option<u16>) -> Self {
|
||||
self.maxlength.alter_opt(maxlength);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece o elimina el texto indicativo del área de texto (`None` para quitarlo).
|
||||
///
|
||||
/// Este texto aparece en el área de texto y desaparece en cuanto el usuario empieza a escribir.
|
||||
/// Al ser texto visible para el usuario se acepta [`L10n`] para poder localizarlo.
|
||||
#[builder_fn]
|
||||
pub fn with_placeholder(mut self, placeholder: impl Into<Option<L10n>>) -> Self {
|
||||
self.placeholder.alter_opt(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece la configuración de autocompletado del campo.
|
||||
///
|
||||
/// Permite al navegador sugerir o rellenar automáticamente el contenido del área de texto
|
||||
/// con valores guardados. Es especialmente útil en áreas con contenido semántico predefinido.
|
||||
///
|
||||
/// Usa los métodos de [`form::Autocomplete`] para los valores más habituales. Pasa `None` para
|
||||
/// omitir el atributo.
|
||||
#[builder_fn]
|
||||
pub fn with_autocomplete(mut self, autocomplete: Option<form::Autocomplete>) -> Self {
|
||||
self.autocomplete.alter_opt(autocomplete);
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo recibe el foco automáticamente al cargar la página.
|
||||
#[builder_fn]
|
||||
pub fn with_autofocus(mut self, autofocus: bool) -> Self {
|
||||
self.autofocus = autofocus;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es de sólo lectura.
|
||||
#[builder_fn]
|
||||
pub fn with_readonly(mut self, readonly: bool) -> Self {
|
||||
self.readonly = readonly;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo es obligatorio.
|
||||
#[builder_fn]
|
||||
pub fn with_required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
self
|
||||
}
|
||||
|
||||
/// Establece si el campo está deshabilitado.
|
||||
#[builder_fn]
|
||||
pub fn with_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ use crate::prelude::*;
|
|||
|
||||
const DEFAULT_VIEWBOX: &str = "0 0 16 16";
|
||||
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone)]
|
||||
pub enum IconKind {
|
||||
#[default]
|
||||
None,
|
||||
|
|
@ -13,20 +13,20 @@ pub enum IconKind {
|
|||
},
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Icon {
|
||||
classes : AttrClasses,
|
||||
icon_kind : IconKind,
|
||||
/// Devuelve las clases CSS asociadas al icono.
|
||||
classes: Classes,
|
||||
icon_kind: IconKind,
|
||||
aria_label: AttrL10n,
|
||||
}
|
||||
|
||||
impl Component for Icon {
|
||||
fn new() -> Self {
|
||||
Icon::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if !matches!(self.icon_kind(), IconKind::None) {
|
||||
self.alter_classes(ClassesOp::Prepend, "icon");
|
||||
}
|
||||
|
|
@ -35,26 +35,26 @@ impl Component for Icon {
|
|||
}
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.icon_kind() {
|
||||
IconKind::None => PrepareMarkup::None,
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.icon_kind() {
|
||||
IconKind::None => html! {},
|
||||
IconKind::Font(_) => {
|
||||
let aria_label = self.aria_label().lookup(cx);
|
||||
let has_label = aria_label.is_some();
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
i
|
||||
class=[self.classes().get()]
|
||||
role=[has_label.then_some("img")]
|
||||
aria-label=[aria_label]
|
||||
aria-hidden=[(!has_label).then_some("true")]
|
||||
{}
|
||||
})
|
||||
}
|
||||
}
|
||||
IconKind::Svg { shapes, viewbox } => {
|
||||
let aria_label = self.aria_label().lookup(cx);
|
||||
let has_label = aria_label.is_some();
|
||||
let viewbox = viewbox.get().unwrap_or_else(|| DEFAULT_VIEWBOX.to_string());
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox=(viewbox)
|
||||
|
|
@ -67,30 +67,30 @@ impl Component for Icon {
|
|||
{
|
||||
(shapes)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn font() -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Font(FontSize::default()))
|
||||
Self::default().with_icon_kind(IconKind::Font(FontSize::default()))
|
||||
}
|
||||
|
||||
pub fn font_sized(font_size: FontSize) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Font(font_size))
|
||||
Self::default().with_icon_kind(IconKind::Font(font_size))
|
||||
}
|
||||
|
||||
pub fn svg(shapes: Markup) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Svg {
|
||||
Self::default().with_icon_kind(IconKind::Svg {
|
||||
shapes,
|
||||
viewbox: AttrValue::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn svg_with_viewbox(shapes: Markup, viewbox: impl AsRef<str>) -> Self {
|
||||
Icon::default().with_icon_kind(IconKind::Svg {
|
||||
Self::default().with_icon_kind(IconKind::Svg {
|
||||
shapes,
|
||||
viewbox: AttrValue::new(viewbox),
|
||||
})
|
||||
|
|
@ -116,19 +116,4 @@ impl Icon {
|
|||
self.aria_label.alter_value(label);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Icon GETTERS >***************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al icono.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
pub fn icon_kind(&self) -> &IconKind {
|
||||
&self.icon_kind
|
||||
}
|
||||
|
||||
pub fn aria_label(&self) -> &AttrL10n {
|
||||
&self.aria_label
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,36 +9,40 @@ use crate::prelude::*;
|
|||
/// ([`classes::Border`](crate::theme::classes::Border)) y **redondeo de esquinas**
|
||||
/// ([`classes::Rounded`](crate::theme::classes::Rounded)).
|
||||
/// - Resuelve el texto alternativo `alt` con **localización** mediante [`L10n`].
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Image {
|
||||
id : AttrId,
|
||||
classes: AttrClasses,
|
||||
size : image::Size,
|
||||
source : image::Source,
|
||||
alt : AttrL10n,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas a la imagen.
|
||||
classes: Classes,
|
||||
/// Devuelve las dimensiones de la imagen.
|
||||
size: image::Size,
|
||||
/// Devuelve el origen de la imagen.
|
||||
source: image::Source,
|
||||
/// Devuelve el texto alternativo localizado.
|
||||
alternative: Attr<L10n>,
|
||||
}
|
||||
|
||||
impl Component for Image {
|
||||
fn new() -> Self {
|
||||
Image::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.source().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let dimensions = self.size().to_style();
|
||||
let alt_text = self.alternative().lookup(cx).unwrap_or_default();
|
||||
let is_decorative = alt_text.is_empty();
|
||||
let source = match self.source() {
|
||||
image::Source::Logo(logo) => {
|
||||
return PrepareMarkup::With(html! {
|
||||
return Ok(html! {
|
||||
span
|
||||
id=[self.id()]
|
||||
class=[self.classes().get()]
|
||||
|
|
@ -55,7 +59,7 @@ impl Component for Image {
|
|||
image::Source::Thumbnail(source) => Some(source),
|
||||
image::Source::Plain(source) => Some(source),
|
||||
};
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
img
|
||||
src=[source]
|
||||
alt=(alt_text)
|
||||
|
|
@ -69,7 +73,7 @@ impl Component for Image {
|
|||
impl Image {
|
||||
/// Crea rápidamente una imagen especificando su origen.
|
||||
pub fn with(source: image::Source) -> Self {
|
||||
Image::default().with_source(source)
|
||||
Self::default().with_source(source)
|
||||
}
|
||||
|
||||
// **< Image BUILDER >**************************************************************************
|
||||
|
|
@ -77,7 +81,7 @@ impl Image {
|
|||
/// Establece el identificador único (`id`) de la imagen.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +93,7 @@ impl Image {
|
|||
/// - Redondear las esquinas ([`classes::Rounded`]).
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -107,35 +111,13 @@ impl Image {
|
|||
self
|
||||
}
|
||||
|
||||
/// Define el texto alternativo localizado ([`L10n`]) para la imagen.
|
||||
/// Define un *texto localizado* ([`L10n`]) alternativo para la imagen.
|
||||
///
|
||||
/// Se recomienda siempre aportar un texto alternativo salvo que la imagen sea puramente
|
||||
/// decorativa.
|
||||
#[builder_fn]
|
||||
pub fn with_alternative(mut self, alt: L10n) -> Self {
|
||||
self.alt.alter_value(alt);
|
||||
self.alternative.alter_value(alt);
|
||||
self
|
||||
}
|
||||
|
||||
// **< Image GETTERS >**************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas a la imagen.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve las dimensiones de la imagen.
|
||||
pub fn size(&self) -> &image::Size {
|
||||
&self.size
|
||||
}
|
||||
|
||||
/// Devuelve el origen de la imagen.
|
||||
pub fn source(&self) -> &image::Source {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// Devuelve el texto alternativo localizado.
|
||||
pub fn alternative(&self) -> &AttrL10n {
|
||||
&self.alt
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ pub enum Size {
|
|||
}
|
||||
|
||||
impl Size {
|
||||
// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
|
||||
/// Devuelve el valor del atributo `style` en función del tamaño, o `None` si no aplica.
|
||||
#[inline]
|
||||
pub(crate) fn to_style(self) -> Option<String> {
|
||||
match self {
|
||||
|
|
@ -54,23 +54,47 @@ pub enum Source {
|
|||
Logo(PageTopSvg),
|
||||
/// Imagen que se adapta automáticamente a su contenedor.
|
||||
///
|
||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
||||
Responsive(String),
|
||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||
Responsive(CowStr),
|
||||
/// Imagen que aplica el estilo **miniatura** de Bootstrap.
|
||||
///
|
||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
||||
Thumbnail(String),
|
||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||
Thumbnail(CowStr),
|
||||
/// Imagen sin clases específicas de Bootstrap, útil para controlar con CSS propio.
|
||||
///
|
||||
/// El `String` asociado es la URL (o ruta) de la imagen.
|
||||
Plain(String),
|
||||
/// Lleva asociada la URL (o ruta) de la imagen.
|
||||
Plain(CowStr),
|
||||
}
|
||||
|
||||
impl Source {
|
||||
const IMG_FLUID: &str = "img-fluid";
|
||||
const IMG_THUMBNAIL: &str = "img-thumbnail";
|
||||
|
||||
// Devuelve la clase base asociada a la imagen según la fuente.
|
||||
/// Imagen con el logotipo de PageTop.
|
||||
#[inline]
|
||||
pub fn logo(svg: PageTopSvg) -> Self {
|
||||
Self::Logo(svg)
|
||||
}
|
||||
|
||||
/// Imagen responsive (`img-fluid`).
|
||||
#[inline]
|
||||
pub fn responsive(url: impl Into<CowStr>) -> Self {
|
||||
Self::Responsive(url.into())
|
||||
}
|
||||
|
||||
/// Imagen miniatura (`img-thumbnail`).
|
||||
#[inline]
|
||||
pub fn thumbnail(url: impl Into<CowStr>) -> Self {
|
||||
Self::Thumbnail(url.into())
|
||||
}
|
||||
|
||||
/// Imagen sin clases adicionales.
|
||||
#[inline]
|
||||
pub fn plain(url: impl Into<CowStr>) -> Self {
|
||||
Self::Plain(url.into())
|
||||
}
|
||||
|
||||
/// Devuelve la clase base asociada a la imagen según la fuente.
|
||||
#[inline]
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -93,9 +117,8 @@ impl Source {
|
|||
classes.push_str(s);
|
||||
} */
|
||||
|
||||
// Devuelve la clase asociada a la imagen según la fuente.
|
||||
#[inline]
|
||||
pub(crate) fn to_class(&self) -> String {
|
||||
/// Devuelve la clase asociada a la imagen según la fuente.
|
||||
pub fn to_class(&self) -> String {
|
||||
let s = self.as_str();
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
|
|
|
|||
|
|
@ -14,17 +14,17 @@
|
|||
//! # 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(
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link_blank(L10n::n("External"), |_| "https://docs.rs".into()))
|
||||
//! .with_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")),
|
||||
//! .with_item(ChildOp::AddMany(vec![
|
||||
//! dropdown::Item::link(L10n::n("Action"), |_| "/action".into()).into(),
|
||||
//! dropdown::Item::link(L10n::n("Another"), |_| "/another".into()).into(),
|
||||
//! ])),
|
||||
//! ))
|
||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"));
|
||||
//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()));
|
||||
//! ```
|
||||
|
||||
mod props;
|
||||
|
|
|
|||
|
|
@ -10,26 +10,30 @@ use crate::prelude::*;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`nav`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Nav {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
items : Children,
|
||||
nav_kind : nav::Kind,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al menú.
|
||||
classes: Classes,
|
||||
/// Devuelve el estilo visual seleccionado.
|
||||
nav_kind: nav::Kind,
|
||||
/// Devuelve la distribución y orientación seleccionada.
|
||||
nav_layout: nav::Layout,
|
||||
/// Devuelve la lista de elementos del menú.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
impl Component for Nav {
|
||||
fn new() -> Self {
|
||||
Nav::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, {
|
||||
let mut classes = "nav".to_string();
|
||||
self.nav_kind().push_class(&mut classes);
|
||||
|
|
@ -38,13 +42,13 @@ impl Component for Nav {
|
|||
});
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
Ok(html! {
|
||||
ul id=[self.id()] class=[self.classes().get()] {
|
||||
(items)
|
||||
}
|
||||
|
|
@ -55,17 +59,17 @@ impl Component for Nav {
|
|||
impl Nav {
|
||||
/// Crea un `Nav` usando pestañas para los elementos (*Tabs*).
|
||||
pub fn tabs() -> Self {
|
||||
Nav::default().with_kind(nav::Kind::Tabs)
|
||||
Self::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)
|
||||
Self::default().with_kind(nav::Kind::Pills)
|
||||
}
|
||||
|
||||
/// Crea un `Nav` usando elementos subrayados (*Underline*).
|
||||
pub fn underline() -> Self {
|
||||
Nav::default().with_kind(nav::Kind::Underline)
|
||||
Self::default().with_kind(nav::Kind::Underline)
|
||||
}
|
||||
|
||||
// **< Nav BUILDER >****************************************************************************
|
||||
|
|
@ -73,14 +77,14 @@ impl Nav {
|
|||
/// 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.id.alter_id(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.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -98,38 +102,21 @@ impl Nav {
|
|||
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`].
|
||||
/// Añade un nuevo elemento al menú o modifica la lista de elementos del menú con una operación
|
||||
/// [`ChildOp`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// nav.with_item(nav::Item::link("Inicio", "/"));
|
||||
/// nav.with_item(ChildOp::AddMany(vec![
|
||||
/// nav::Item::link(...).into(),
|
||||
/// nav::Item::link_disabled(...).into(),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
pub fn with_items(mut self, op: TypedOp<nav::Item>) -> Self {
|
||||
self.items.alter_typed(op);
|
||||
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.items.alter_child(op.into());
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,23 +10,27 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// Define internamente la naturaleza del elemento y su comportamiento al mostrarse o interactuar
|
||||
/// con él.
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
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.
|
||||
/// Elemento de navegación basado en una [`RoutePath`] dinámica devuelta por
|
||||
/// [`FnPathByContext`]. Opcionalmente, puede abrirse en una nueva ventana y estar inicialmente
|
||||
/// deshabilitado.
|
||||
Link {
|
||||
label: L10n,
|
||||
path: FnPathByContext,
|
||||
route: FnPathByContext,
|
||||
blank: bool,
|
||||
disabled: bool,
|
||||
},
|
||||
/// Contenido HTML arbitrario. El componente [`Html`] se renderiza tal cual como elemento del
|
||||
/// menú, sin añadir ningún comportamiento de navegación adicional.
|
||||
Html(Embed<Html>),
|
||||
/// Elemento que despliega un menú [`Dropdown`].
|
||||
Dropdown(Typed<Dropdown>),
|
||||
Dropdown(Embed<Dropdown>),
|
||||
}
|
||||
|
||||
impl ItemKind {
|
||||
|
|
@ -56,7 +60,7 @@ impl ItemKind {
|
|||
classes.push_str(class);
|
||||
} */
|
||||
|
||||
// Devuelve las clases asociadas al tipo de elemento.
|
||||
/// Devuelve las clases asociadas al tipo de elemento.
|
||||
#[inline]
|
||||
pub(crate) fn to_class(&self) -> String {
|
||||
self.as_str().to_owned()
|
||||
|
|
@ -68,52 +72,54 @@ impl ItemKind {
|
|||
/// 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`].
|
||||
/// puede comportarse como texto, enlace, contenido HTML 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)]
|
||||
/// Permite definir el identificador, las clases de estilo adicionales y el tipo de interacción
|
||||
/// asociada, manteniendo una interfaz común para renderizar todos los elementos del menú.
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Item {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al elemento.
|
||||
classes: Classes,
|
||||
/// Devuelve el tipo de elemento representado.
|
||||
item_kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, self.item_kind().to_class());
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self.item_kind() {
|
||||
ItemKind::Void => PrepareMarkup::None,
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self.item_kind() {
|
||||
ItemKind::Void => html! {},
|
||||
|
||||
ItemKind::Label(label) => PrepareMarkup::With(html! {
|
||||
ItemKind::Label(label) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
span class="nav-link disabled" aria-disabled="true" {
|
||||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank,
|
||||
disabled,
|
||||
} => {
|
||||
let path = path(cx);
|
||||
let route_link = route(cx);
|
||||
let current_path = cx.request().map(|request| request.path());
|
||||
let is_current = !*disabled && (current_path == Some(path));
|
||||
let is_current = !*disabled && (current_path == Some(route_link.path()));
|
||||
|
||||
let mut classes = "nav-link".to_string();
|
||||
if is_current {
|
||||
|
|
@ -123,14 +129,14 @@ impl Component for Item {
|
|||
classes.push_str(" disabled");
|
||||
}
|
||||
|
||||
let href = (!*disabled).then_some(path);
|
||||
let href = (!*disabled).then_some(route_link);
|
||||
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! {
|
||||
html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class=(classes)
|
||||
|
|
@ -143,21 +149,27 @@ impl Component for Item {
|
|||
(label.using(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ItemKind::Html(html) => html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
(html.render(cx))
|
||||
}
|
||||
},
|
||||
|
||||
ItemKind::Dropdown(menu) => {
|
||||
if let Some(dd) = menu.borrow() {
|
||||
if let Some(dd) = menu.get() {
|
||||
let items = dd.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
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! {
|
||||
html! {
|
||||
li id=[self.id()] class=[self.classes().get()] {
|
||||
a
|
||||
class="nav-link dropdown-toggle"
|
||||
|
|
@ -172,30 +184,34 @@ impl Component for Item {
|
|||
(items)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
PrepareMarkup::None
|
||||
}
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Item {
|
||||
/// Crea un elemento de tipo texto, mostrado sin interacción.
|
||||
pub fn label(label: L10n) -> Self {
|
||||
Item {
|
||||
Self {
|
||||
item_kind: ItemKind::Label(label),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea un enlace para la navegación.
|
||||
pub fn link(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
///
|
||||
/// La ruta se obtiene invocando [`FnPathByContext`], que devuelve dinámicamente una
|
||||
/// [`RoutePath`] en función del [`Context`]. El enlace se marca como `active` si la ruta actual
|
||||
/// del *request* coincide con la ruta de destino (devuelta por `RoutePath::path`).
|
||||
pub fn link(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: false,
|
||||
},
|
||||
|
|
@ -204,11 +220,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace deshabilitado que no permite la interacción.
|
||||
pub fn link_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: false,
|
||||
disabled: true,
|
||||
},
|
||||
|
|
@ -217,11 +233,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace que se abre en una nueva ventana o pestaña.
|
||||
pub fn link_blank(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_blank(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: false,
|
||||
},
|
||||
|
|
@ -230,11 +246,11 @@ impl Item {
|
|||
}
|
||||
|
||||
/// Crea un enlace inicialmente deshabilitado que se abriría en una nueva ventana.
|
||||
pub fn link_blank_disabled(label: L10n, path: FnPathByContext) -> Self {
|
||||
Item {
|
||||
pub fn link_blank_disabled(label: L10n, route: FnPathByContext) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Link {
|
||||
label,
|
||||
path,
|
||||
route,
|
||||
blank: true,
|
||||
disabled: true,
|
||||
},
|
||||
|
|
@ -242,14 +258,25 @@ impl Item {
|
|||
}
|
||||
}
|
||||
|
||||
/// Crea un elemento con contenido HTML arbitrario.
|
||||
///
|
||||
/// El contenido se renderiza tal cual lo devuelve el componente [`Html`], dentro de un `<li>`
|
||||
/// con las clases de navegación asociadas a [`Item`].
|
||||
pub fn html(html: Html) -> Self {
|
||||
Self {
|
||||
item_kind: ItemKind::Html(Embed::with(html)),
|
||||
..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`].
|
||||
/// Sólo se tienen en cuenta **el título** (si no existe, se 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)),
|
||||
Self {
|
||||
item_kind: ItemKind::Dropdown(Embed::with(menu)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -259,26 +286,14 @@ impl Item {
|
|||
/// 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.id.alter_id(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.classes.alter_classes(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.
|
||||
pub fn item_kind(&self) -> &ItemKind {
|
||||
&self.item_kind
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ impl Kind {
|
|||
const PILLS: &str = "nav-pills";
|
||||
const UNDERLINE: &str = "nav-underline";
|
||||
|
||||
// Devuelve la clase base asociada al tipo de menú, o una cadena vacía si no aplica.
|
||||
/// Devuelve la clase base asociada al tipo de menú, o una cadena vacía si no aplica.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
|
|
@ -33,7 +33,7 @@ impl Kind {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada al tipo de menú a la cadena de clases.
|
||||
/// Añade la clase asociada al tipo de menú a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
|
|
@ -83,7 +83,7 @@ impl Layout {
|
|||
const FILL: &str = "nav-fill";
|
||||
const JUSTIFIED: &str = "nav-justified";
|
||||
|
||||
// Devuelve la clase base asociada a la distribución y orientación del menú.
|
||||
/// Devuelve la clase base asociada a la distribución y orientación del menú.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
|
|
@ -98,7 +98,7 @@ impl Layout {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada a la distribución y orientación del menú a la cadena de clases.
|
||||
/// Añade la clase asociada a la distribución y orientación del menú a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
|
|
@ -112,7 +112,7 @@ impl Layout {
|
|||
}
|
||||
|
||||
/* Devuelve la clase asociada a la distribución y orientación del menú, o una cadena vacía si no
|
||||
// aplica (reservado).
|
||||
/// aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_owned()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Cada [`navbar::Item`](crate::theme::navbar::Item) representa un elemento individual de la barra
|
||||
//! de navegación [`Navbar`], con distintos comportamientos según su finalidad, como menús
|
||||
//! [`Nav`](crate::theme::Nav) o textos localizados usando [`L10n`](pagetop::locale::L10n).
|
||||
//! [`Nav`](crate::theme::Nav) o *textos localizados* usando [`L10n`](pagetop::locale::L10n).
|
||||
//!
|
||||
//! También puede mostrar una marca de identidad ([`navbar::Brand`](crate::theme::navbar::Brand))
|
||||
//! que identifique la compañía, producto o nombre del proyecto asociado a la solución web.
|
||||
|
|
@ -15,11 +15,11 @@
|
|||
//! # use pagetop::prelude::*;
|
||||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let navbar = Navbar::simple()
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::link(L10n::n("About"), |_| "/about"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Contact"), |_| "/contact"))
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("About"), |_| "/about".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Contact"), |_| "/contact".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -30,11 +30,11 @@
|
|||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let navbar = Navbar::simple_toggle()
|
||||
//! .with_expand(BreakPoint::MD)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.example.com"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Support"), |_| "/support"))
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Support"), |_| "/support".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -45,19 +45,23 @@
|
|||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("PageTop"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_left(brand)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::dropdown(
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::dropdown(
|
||||
//! Dropdown::new()
|
||||
//! .with_title(L10n::n("Tools"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Generator"), |_| "/tools/gen"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Reports"), |_| "/tools/reports"))
|
||||
//! .with_item(dropdown::Item::link(
|
||||
//! L10n::n("Generator"), |_| "/tools/gen".into())
|
||||
//! )
|
||||
//! .with_item(dropdown::Item::link(
|
||||
//! L10n::n("Reports"), |_| "/tools/reports".into())
|
||||
//! )
|
||||
//! ))
|
||||
//! .add_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#"))
|
||||
//! .with_item(nav::Item::link_disabled(L10n::n("Disabled"), |_| "#".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -68,14 +72,14 @@
|
|||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("Intranet"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_right(brand)
|
||||
//! .with_expand(BreakPoint::LG)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::pills()
|
||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Users"), |_| "/users"))
|
||||
//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/dashboard".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Users"), |_| "/users".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
//!
|
||||
|
|
@ -91,15 +95,15 @@
|
|||
//! .with_backdrop(offcanvas::Backdrop::Enabled);
|
||||
//!
|
||||
//! let navbar = Navbar::offcanvas(oc)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Home"), |_| "/"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Profile"), |_| "/profile"))
|
||||
//! .add_item(nav::Item::dropdown(
|
||||
//! .with_item(nav::Item::link(L10n::n("Home"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Profile"), |_| "/profile".into()))
|
||||
//! .with_item(nav::Item::dropdown(
|
||||
//! Dropdown::new()
|
||||
//! .with_title(L10n::n("More"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Help"), |_| "/help"))
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Settings"), |_| "/settings".into()))
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Help"), |_| "/help".into()))
|
||||
//! ))
|
||||
//! ));
|
||||
//! ```
|
||||
|
|
@ -111,15 +115,15 @@
|
|||
//! # use pagetop_bootsier::prelude::*;
|
||||
//! let brand = navbar::Brand::new()
|
||||
//! .with_title(L10n::n("Main App"))
|
||||
//! .with_path(Some(|_| "/"));
|
||||
//! .with_route(Some(|cx| cx.route("/")));
|
||||
//!
|
||||
//! let navbar = Navbar::brand_left(brand)
|
||||
//! .with_position(navbar::Position::FixedTop)
|
||||
//! .add_item(navbar::Item::nav(
|
||||
//! .with_item(navbar::Item::nav(
|
||||
//! Nav::new()
|
||||
//! .add_item(nav::Item::link(L10n::n("Dashboard"), |_| "/"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Donors"), |_| "/donors"))
|
||||
//! .add_item(nav::Item::link(L10n::n("Stock"), |_| "/stock"))
|
||||
//! .with_item(nav::Item::link(L10n::n("Dashboard"), |_| "/".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Donors"), |_| "/donors".into()))
|
||||
//! .with_item(nav::Item::link(L10n::n("Stock"), |_| "/stock".into()))
|
||||
//! ));
|
||||
//! ```
|
||||
|
||||
|
|
|
|||
|
|
@ -6,42 +6,46 @@ use crate::prelude::*;
|
|||
///
|
||||
/// Representa la identidad del sitio con una imagen, título y eslogan:
|
||||
///
|
||||
/// - Si hay URL ([`with_path()`](Self::with_path)), el bloque completo actúa como enlace. Por
|
||||
/// - Si hay URL ([`with_route()`](Self::with_route)), el bloque completo actúa como enlace. Por
|
||||
/// defecto enlaza a la raíz del sitio (`/`).
|
||||
/// - Si no hay imagen ([`with_image()`](Self::with_image)) ni título
|
||||
/// ([`with_title()`](Self::with_title)), la marca de identidad no se renderiza.
|
||||
/// - El eslogan ([`with_slogan()`](Self::with_slogan)) es opcional; por defecto no tiene contenido.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Brand {
|
||||
id : AttrId,
|
||||
image : Typed<Image>,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve la imagen de marca (si la hay).
|
||||
image: Embed<Image>,
|
||||
/// Devuelve el título de la identidad de marca.
|
||||
#[default(_code = "L10n::n(&global::SETTINGS.app.name)")]
|
||||
title : L10n,
|
||||
title: L10n,
|
||||
/// Devuelve el eslogan de la marca.
|
||||
slogan: L10n,
|
||||
#[default(_code = "Some(|_| \"/\")")]
|
||||
path : Option<FnPathByContext>,
|
||||
/// Devuelve la función que resuelve la URL asociada a la marca (si existe).
|
||||
#[default(_code = "Some(|cx| cx.route(\"/\"))")]
|
||||
route: Option<FnPathByContext>,
|
||||
}
|
||||
|
||||
impl Component for Brand {
|
||||
fn new() -> Self {
|
||||
Brand::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
let image = self.image().render(cx);
|
||||
let title = self.title().using(cx);
|
||||
if title.is_empty() && image.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
let slogan = self.slogan().using(cx);
|
||||
PrepareMarkup::With(html! {
|
||||
@if let Some(path) = self.path() {
|
||||
a class="navbar-brand" href=(path(cx)) { (image) (title) (slogan) }
|
||||
Ok(html! {
|
||||
@if let Some(route) = self.route() {
|
||||
a class="navbar-brand" href=(route(cx)) { (image) (title) (slogan) }
|
||||
} @else {
|
||||
span class="navbar-brand" { (image) (title) (slogan) }
|
||||
}
|
||||
|
|
@ -55,7 +59,7 @@ impl Brand {
|
|||
/// Establece el identificador único (`id`) de la marca.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -82,30 +86,8 @@ impl Brand {
|
|||
|
||||
/// Define la URL de destino. Si es `None`, la marca no será un enlace.
|
||||
#[builder_fn]
|
||||
pub fn with_path(mut self, path: Option<FnPathByContext>) -> Self {
|
||||
self.path = path;
|
||||
pub fn with_route(mut self, route: Option<FnPathByContext>) -> Self {
|
||||
self.route = route;
|
||||
self
|
||||
}
|
||||
|
||||
// **< Brand GETTERS >**************************************************************************
|
||||
|
||||
/// Devuelve la imagen de marca (si la hay).
|
||||
pub fn image(&self) -> &Typed<Image> {
|
||||
&self.image
|
||||
}
|
||||
|
||||
/// Devuelve el título de la identidad de marca.
|
||||
pub fn title(&self) -> &L10n {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Devuelve el eslogan de la marca.
|
||||
pub fn slogan(&self) -> &L10n {
|
||||
&self.slogan
|
||||
}
|
||||
|
||||
/// Devuelve la función que resuelve la URL asociada a la marca (si existe).
|
||||
pub fn path(&self) -> &Option<FnPathByContext> {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,27 +14,32 @@ const TOGGLE_OFFCANVAS: &str = "offcanvas";
|
|||
///
|
||||
/// Ver ejemplos en el módulo [`navbar`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Navbar {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
expand : BreakPoint,
|
||||
layout : navbar::Layout,
|
||||
position : navbar::Position,
|
||||
items : Children,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas a la barra de navegación.
|
||||
classes: Classes,
|
||||
/// Devuelve el punto de ruptura configurado.
|
||||
expand: BreakPoint,
|
||||
/// Devuelve la disposición configurada para la barra de navegación.
|
||||
layout: navbar::Layout,
|
||||
/// Devuelve la posición configurada para la barra de navegación.
|
||||
position: navbar::Position,
|
||||
/// Devuelve la lista de contenidos.
|
||||
items: Children,
|
||||
}
|
||||
|
||||
impl Component for Navbar {
|
||||
fn new() -> Self {
|
||||
Navbar::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, {
|
||||
let mut classes = "navbar".to_string();
|
||||
self.expand().push_class(&mut classes, "navbar-expand", "");
|
||||
|
|
@ -43,10 +48,10 @@ impl Component for Navbar {
|
|||
});
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
// Botón de despliegue (colapso u offcanvas) para la barra.
|
||||
fn button(cx: &mut Context, data_bs_toggle: &str, id_content: &str) -> Markup {
|
||||
let id_content_target = join!("#", id_content);
|
||||
let id_content_target = util::join!("#", id_content);
|
||||
let aria_expanded = if data_bs_toggle == TOGGLE_COLLAPSE {
|
||||
Some("false")
|
||||
} else {
|
||||
|
|
@ -70,14 +75,14 @@ impl Component for Navbar {
|
|||
// Si no hay contenidos, no tiene sentido mostrar una barra vacía.
|
||||
let items = self.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
|
||||
// Asegura que la barra tiene un id estable para poder asociarlo al colapso/offcanvas.
|
||||
let id = cx.required_id::<Self>(self.id());
|
||||
// Asegura que la barra tiene un `id` para poder asociarlo al colapso/offcanvas.
|
||||
let id = cx.required_id::<Self>(self.id(), 1);
|
||||
|
||||
PrepareMarkup::With(html! {
|
||||
nav id=(id) class=[self.classes().get()] {
|
||||
Ok(html! {
|
||||
nav id=(&id) class=[self.classes().get()] {
|
||||
div class="container-fluid" {
|
||||
@match self.layout() {
|
||||
// Barra más sencilla: sólo contenido.
|
||||
|
|
@ -87,10 +92,10 @@ impl Component for Navbar {
|
|||
|
||||
// Barra sencilla que se puede contraer/expandir.
|
||||
navbar::Layout::SimpleToggle => {
|
||||
@let id_content = join!(id, "-content");
|
||||
@let id_content = util::join!(id, "-content");
|
||||
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
div id=(id_content) class="collapse navbar-collapse" {
|
||||
div id=(&id_content) class="collapse navbar-collapse" {
|
||||
(items)
|
||||
}
|
||||
},
|
||||
|
|
@ -103,22 +108,22 @@ impl Component for Navbar {
|
|||
|
||||
// Barra con marca a la izquierda y botón a la derecha.
|
||||
navbar::Layout::BrandLeft(brand) => {
|
||||
@let id_content = join!(id, "-content");
|
||||
@let id_content = util::join!(id, "-content");
|
||||
|
||||
(brand.render(cx))
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
div id=(id_content) class="collapse navbar-collapse" {
|
||||
div id=(&id_content) class="collapse navbar-collapse" {
|
||||
(items)
|
||||
}
|
||||
},
|
||||
|
||||
// Barra con botón a la izquierda y marca a la derecha.
|
||||
navbar::Layout::BrandRight(brand) => {
|
||||
@let id_content = join!(id, "-content");
|
||||
@let id_content = util::join!(id, "-content");
|
||||
|
||||
(button(cx, TOGGLE_COLLAPSE, &id_content))
|
||||
(brand.render(cx))
|
||||
div id=(id_content) class="collapse navbar-collapse" {
|
||||
div id=(&id_content) class="collapse navbar-collapse" {
|
||||
(items)
|
||||
}
|
||||
},
|
||||
|
|
@ -128,7 +133,7 @@ impl Component for Navbar {
|
|||
@let id_content = offcanvas.id().unwrap_or_default();
|
||||
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
@if let Some(oc) = offcanvas.borrow() {
|
||||
@if let Some(oc) = offcanvas.get() {
|
||||
(oc.render_offcanvas(cx, Some(self.items())))
|
||||
}
|
||||
},
|
||||
|
|
@ -139,7 +144,7 @@ impl Component for Navbar {
|
|||
|
||||
(brand.render(cx))
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
@if let Some(oc) = offcanvas.borrow() {
|
||||
@if let Some(oc) = offcanvas.get() {
|
||||
(oc.render_offcanvas(cx, Some(self.items())))
|
||||
}
|
||||
},
|
||||
|
|
@ -150,7 +155,7 @@ impl Component for Navbar {
|
|||
|
||||
(button(cx, TOGGLE_OFFCANVAS, &id_content))
|
||||
(brand.render(cx))
|
||||
@if let Some(oc) = offcanvas.borrow() {
|
||||
@if let Some(oc) = offcanvas.get() {
|
||||
(oc.render_offcanvas(cx, Some(self.items())))
|
||||
}
|
||||
},
|
||||
|
|
@ -164,47 +169,47 @@ impl Component for Navbar {
|
|||
impl Navbar {
|
||||
/// Crea una barra de navegación **simple**, sin marca y sin botón.
|
||||
pub fn simple() -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::Simple)
|
||||
Self::default().with_layout(navbar::Layout::Simple)
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación **simple pero colapsable**, con botón a la izquierda.
|
||||
pub fn simple_toggle() -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::SimpleToggle)
|
||||
Self::default().with_layout(navbar::Layout::SimpleToggle)
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación **con marca a la izquierda**, siempre visible.
|
||||
pub fn simple_brand_left(brand: navbar::Brand) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::SimpleBrandLeft(Typed::with(brand)))
|
||||
Self::default().with_layout(navbar::Layout::SimpleBrandLeft(Embed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **marca a la izquierda** y **botón a la derecha**.
|
||||
pub fn brand_left(brand: navbar::Brand) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::BrandLeft(Typed::with(brand)))
|
||||
Self::default().with_layout(navbar::Layout::BrandLeft(Embed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **botón a la izquierda** y **marca a la derecha**.
|
||||
pub fn brand_right(brand: navbar::Brand) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::BrandRight(Typed::with(brand)))
|
||||
Self::default().with_layout(navbar::Layout::BrandRight(Embed::with(brand)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación cuyo contenido se muestra en un **offcanvas**.
|
||||
pub fn offcanvas(oc: Offcanvas) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::Offcanvas(Typed::with(oc)))
|
||||
Self::default().with_layout(navbar::Layout::Offcanvas(Embed::with(oc)))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **marca a la izquierda** y contenido en **offcanvas**.
|
||||
pub fn offcanvas_brand_left(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
|
||||
Typed::with(brand),
|
||||
Typed::with(oc),
|
||||
Self::default().with_layout(navbar::Layout::OffcanvasBrandLeft(
|
||||
Embed::with(brand),
|
||||
Embed::with(oc),
|
||||
))
|
||||
}
|
||||
|
||||
/// Crea una barra de navegación con **marca a la derecha** y contenido en **offcanvas**.
|
||||
pub fn offcanvas_brand_right(brand: navbar::Brand, oc: Offcanvas) -> Self {
|
||||
Navbar::default().with_layout(navbar::Layout::OffcanvasBrandRight(
|
||||
Typed::with(brand),
|
||||
Typed::with(oc),
|
||||
Self::default().with_layout(navbar::Layout::OffcanvasBrandRight(
|
||||
Embed::with(brand),
|
||||
Embed::with(oc),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +218,7 @@ impl Navbar {
|
|||
/// Establece el identificador único (`id`) de la barra de navegación.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +230,7 @@ impl Navbar {
|
|||
/// - Definir la apariencia del texto ([`classes::Text`]).
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -250,44 +255,21 @@ impl Navbar {
|
|||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo contenido hijo.
|
||||
#[inline]
|
||||
pub fn add_item(mut self, item: navbar::Item) -> Self {
|
||||
self.items.add(Child::with(item));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de contenidos (`children`) aplicando una operación [`TypedOp`].
|
||||
/// Añade un nuevo contenido a la barra de navegación o modifica la lista de contenidos de la
|
||||
/// barra con una operación [`ChildOp`].
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// navbar.with_item(navbar::Item::nav(...));
|
||||
/// navbar.with_item(ChildOp::AddMany(vec![
|
||||
/// navbar::Item::nav(...).into(),
|
||||
/// navbar::Item::text(...).into(),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[builder_fn]
|
||||
pub fn with_items(mut self, op: TypedOp<navbar::Item>) -> Self {
|
||||
self.items.alter_typed(op);
|
||||
pub fn with_item(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.items.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
|
||||
// **< Navbar GETTERS >*************************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas a la barra de navegación.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el punto de ruptura configurado.
|
||||
pub fn expand(&self) -> &BreakPoint {
|
||||
&self.expand
|
||||
}
|
||||
|
||||
/// Devuelve la disposición configurada para la barra de navegación.
|
||||
pub fn layout(&self) -> &navbar::Layout {
|
||||
&self.layout
|
||||
}
|
||||
|
||||
/// Devuelve la posición configurada para la barra de navegación.
|
||||
pub fn position(&self) -> &navbar::Position {
|
||||
&self.position
|
||||
}
|
||||
|
||||
/// Devuelve la lista de contenidos (`children`).
|
||||
pub fn items(&self) -> &Children {
|
||||
&self.items
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use crate::prelude::*;
|
|||
/// Cada variante determina qué se renderiza y cómo. Estos elementos se colocan **dentro del
|
||||
/// contenido** de la barra (la parte colapsable, el *offcanvas* o el bloque simple), por lo que son
|
||||
/// independientes de la marca o del botón que ya pueda definir el propio [`navbar::Layout`].
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum Item {
|
||||
/// Sin contenido, no produce salida.
|
||||
#[default]
|
||||
|
|
@ -17,16 +17,16 @@ pub enum Item {
|
|||
/// Útil cuando el [`navbar::Layout`] no incluye marca, y se quiere incluir dentro del área
|
||||
/// colapsable/*offcanvas*. Si el *layout* ya muestra una marca, esta variante no la sustituye,
|
||||
/// sólo añade otra dentro del bloque de contenidos.
|
||||
Brand(Typed<navbar::Brand>),
|
||||
Brand(Embed<navbar::Brand>),
|
||||
/// Representa un menú de navegación [`Nav`](crate::theme::Nav).
|
||||
Nav(Typed<Nav>),
|
||||
/// Representa un texto libre localizado.
|
||||
Nav(Embed<Nav>),
|
||||
/// Representa un *texto localizado* libre.
|
||||
Text(L10n),
|
||||
}
|
||||
|
||||
impl Component for Item {
|
||||
fn new() -> Self {
|
||||
Item::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
|
|
@ -38,39 +38,39 @@ impl Component for Item {
|
|||
}
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
if let Self::Nav(nav) = self {
|
||||
if let Some(mut nav) = nav.borrow_mut() {
|
||||
if let Some(mut nav) = nav.get() {
|
||||
nav.alter_classes(ClassesOp::Prepend, "navbar-nav");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
match self {
|
||||
Self::Void => PrepareMarkup::None,
|
||||
Self::Brand(brand) => PrepareMarkup::With(html! { (brand.render(cx)) }),
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(match self {
|
||||
Self::Void => html! {},
|
||||
Self::Brand(brand) => html! { (brand.render(cx)) },
|
||||
Self::Nav(nav) => {
|
||||
if let Some(nav) = nav.borrow() {
|
||||
if let Some(nav) = nav.get() {
|
||||
let items = nav.items().render(cx);
|
||||
if items.is_empty() {
|
||||
return PrepareMarkup::None;
|
||||
return Ok(html! {});
|
||||
}
|
||||
PrepareMarkup::With(html! {
|
||||
html! {
|
||||
ul id=[nav.id()] class=[nav.classes().get()] {
|
||||
(items)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
PrepareMarkup::None
|
||||
html! {}
|
||||
}
|
||||
}
|
||||
Self::Text(text) => PrepareMarkup::With(html! {
|
||||
Self::Text(text) => html! {
|
||||
span class="navbar-text" {
|
||||
(text.using(cx))
|
||||
}
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,15 +80,15 @@ impl Item {
|
|||
/// Pensado para barras colapsables u offcanvas donde se quiere que la marca aparezca en la zona
|
||||
/// desplegable.
|
||||
pub fn brand(brand: navbar::Brand) -> Self {
|
||||
Self::Brand(Typed::with(brand))
|
||||
Self::Brand(Embed::with(brand))
|
||||
}
|
||||
|
||||
/// Crea un elemento de tipo [`Nav`] para añadir al contenido de [`Navbar`].
|
||||
pub fn nav(item: Nav) -> Self {
|
||||
Self::Nav(Typed::with(item))
|
||||
Self::Nav(Embed::with(item))
|
||||
}
|
||||
|
||||
/// Crea un elemento de texto localizado, mostrado sin interacción.
|
||||
/// Crea un elemento con un *texto localizado*, mostrado sin interacción.
|
||||
pub fn text(item: L10n) -> Self {
|
||||
Self::Text(item)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::prelude::*;
|
|||
// **< Layout >*************************************************************************************
|
||||
|
||||
/// Representa los diferentes tipos de presentación de una barra de navegación [`Navbar`].
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug)]
|
||||
pub enum Layout {
|
||||
/// Barra simple, sin marca de identidad y sin botón de despliegue.
|
||||
///
|
||||
|
|
@ -19,24 +19,24 @@ pub enum Layout {
|
|||
/// Barra simple, con marca de identidad a la izquierda y sin botón de despliegue.
|
||||
///
|
||||
/// La barra de navegación no se colapsa.
|
||||
SimpleBrandLeft(Typed<navbar::Brand>),
|
||||
SimpleBrandLeft(Embed<navbar::Brand>),
|
||||
|
||||
/// Barra con marca de identidad a la izquierda y botón de despliegue a la derecha.
|
||||
BrandLeft(Typed<navbar::Brand>),
|
||||
BrandLeft(Embed<navbar::Brand>),
|
||||
|
||||
/// Barra con botón de despliegue a la izquierda y marca de identidad a la derecha.
|
||||
BrandRight(Typed<navbar::Brand>),
|
||||
BrandRight(Embed<navbar::Brand>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y sin marca de identidad.
|
||||
Offcanvas(Typed<Offcanvas>),
|
||||
Offcanvas(Embed<Offcanvas>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con marca de identidad a la izquierda y botón de despliegue a la
|
||||
/// derecha.
|
||||
OffcanvasBrandLeft(Typed<navbar::Brand>, Typed<Offcanvas>),
|
||||
OffcanvasBrandLeft(Embed<navbar::Brand>, Embed<Offcanvas>),
|
||||
|
||||
/// Contenido en [`Offcanvas`], con botón de despliegue a la izquierda y marca de identidad a la
|
||||
/// derecha.
|
||||
OffcanvasBrandRight(Typed<navbar::Brand>, Typed<Offcanvas>),
|
||||
OffcanvasBrandRight(Embed<navbar::Brand>, Embed<Offcanvas>),
|
||||
}
|
||||
|
||||
// **< Position >***********************************************************************************
|
||||
|
|
@ -64,7 +64,7 @@ pub enum Position {
|
|||
}
|
||||
|
||||
impl Position {
|
||||
// Devuelve la clase base asociada a la posición de la barra de navegación.
|
||||
/// Devuelve la clase base asociada a la posición de la barra de navegación.
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -76,7 +76,7 @@ impl Position {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases.
|
||||
/// Añade la clase asociada a la posición de la barra de navegación a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
|
|
@ -90,7 +90,7 @@ impl Position {
|
|||
}
|
||||
|
||||
/* Devuelve la clase asociada a la posición de la barra de navegación, o cadena vacía si no
|
||||
// aplica (reservado).
|
||||
/// aplica (reservado).
|
||||
#[inline]
|
||||
pub(crate) fn to_class(self) -> String {
|
||||
self.as_str().to_string()
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@
|
|||
//! .with_backdrop(offcanvas::Backdrop::Enabled)
|
||||
//! .with_body_scroll(offcanvas::BodyScroll::Enabled)
|
||||
//! .with_visibility(offcanvas::Visibility::Default)
|
||||
//! .add_child(Dropdown::new()
|
||||
//! .with_child(Dropdown::new()
|
||||
//! .with_title(L10n::n("Menu"))
|
||||
//! .add_item(dropdown::Item::label(L10n::n("Label")))
|
||||
//! .add_item(dropdown::Item::link_blank(L10n::n("Google"), |_| "https://www.google.es"))
|
||||
//! .add_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout"))
|
||||
//! .with_item(dropdown::Item::label(L10n::n("Label")))
|
||||
//! .with_item(dropdown::Item::link_blank(L10n::n("Docs"), |_| "https://docs.rs".into()))
|
||||
//! .with_item(dropdown::Item::link(L10n::n("Sign out"), |_| "/signout".into()))
|
||||
//! );
|
||||
//! ```
|
||||
|
||||
|
|
|
|||
|
|
@ -21,30 +21,38 @@ use crate::LOCALES_BOOTSIER;
|
|||
///
|
||||
/// Ver ejemplo en el módulo [`offcanvas`].
|
||||
/// Si no contiene elementos, el componente **no se renderiza**.
|
||||
#[rustfmt::skip]
|
||||
#[derive(AutoDefault)]
|
||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||
pub struct Offcanvas {
|
||||
id : AttrId,
|
||||
classes : AttrClasses,
|
||||
title : L10n,
|
||||
#[getters(skip)]
|
||||
id: AttrId,
|
||||
/// Devuelve las clases CSS asociadas al panel.
|
||||
classes: Classes,
|
||||
/// Devuelve el título del panel.
|
||||
title: L10n,
|
||||
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
|
||||
breakpoint: BreakPoint,
|
||||
backdrop : offcanvas::Backdrop,
|
||||
scrolling : offcanvas::BodyScroll,
|
||||
placement : offcanvas::Placement,
|
||||
/// Devuelve el comportamiento configurado para la capa de fondo.
|
||||
backdrop: offcanvas::Backdrop,
|
||||
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
|
||||
body_scroll: offcanvas::BodyScroll,
|
||||
/// Devuelve la posición de inicio del panel.
|
||||
placement: offcanvas::Placement,
|
||||
/// Devuelve el estado inicial del panel.
|
||||
visibility: offcanvas::Visibility,
|
||||
children : Children,
|
||||
/// Devuelve la lista de componentes (`children`) del panel.
|
||||
children: Children,
|
||||
}
|
||||
|
||||
impl Component for Offcanvas {
|
||||
fn new() -> Self {
|
||||
Offcanvas::default()
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<String> {
|
||||
self.id.get()
|
||||
}
|
||||
|
||||
fn setup_before_prepare(&mut self, _cx: &mut Context) {
|
||||
fn setup(&mut self, _cx: &Context) {
|
||||
self.alter_classes(ClassesOp::Prepend, {
|
||||
let mut classes = "offcanvas".to_string();
|
||||
self.breakpoint().push_class(&mut classes, "offcanvas", "");
|
||||
|
|
@ -54,8 +62,8 @@ impl Component for Offcanvas {
|
|||
});
|
||||
}
|
||||
|
||||
fn prepare_component(&self, cx: &mut Context) -> PrepareMarkup {
|
||||
PrepareMarkup::With(self.render_offcanvas(cx, None))
|
||||
fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
Ok(self.render_offcanvas(cx, None))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -65,14 +73,14 @@ impl Offcanvas {
|
|||
/// Establece el identificador único (`id`) del panel.
|
||||
#[builder_fn]
|
||||
pub fn with_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.id.alter_value(id);
|
||||
self.id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de clases CSS aplicadas al panel.
|
||||
#[builder_fn]
|
||||
pub fn with_classes(mut self, op: ClassesOp, classes: impl AsRef<str>) -> Self {
|
||||
self.classes.alter_value(op, classes);
|
||||
self.classes.alter_classes(op, classes);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -109,7 +117,7 @@ impl Offcanvas {
|
|||
/// Permite o bloquea el desplazamiento de la página principal mientras el panel está abierto.
|
||||
#[builder_fn]
|
||||
pub fn with_body_scroll(mut self, scrolling: offcanvas::BodyScroll) -> Self {
|
||||
self.scrolling = scrolling;
|
||||
self.body_scroll = scrolling;
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -127,62 +135,14 @@ impl Offcanvas {
|
|||
self
|
||||
}
|
||||
|
||||
/// Añade un nuevo componente hijo al panel.
|
||||
#[inline]
|
||||
pub fn add_child(mut self, child: impl Component) -> Self {
|
||||
self.children.add(Child::with(child));
|
||||
self
|
||||
}
|
||||
|
||||
/// Modifica la lista de componentes (`children`) aplicando una operación [`ChildOp`].
|
||||
/// Añade un nuevo componente al panel o modifica la lista de componentes (`children`) con una
|
||||
/// operación [`ChildOp`].
|
||||
#[builder_fn]
|
||||
pub fn with_children(mut self, op: ChildOp) -> Self {
|
||||
self.children.alter_child(op);
|
||||
pub fn with_child(mut self, op: impl Into<ChildOp>) -> Self {
|
||||
self.children.alter_child(op.into());
|
||||
self
|
||||
}
|
||||
|
||||
// **< Offcanvas GETTERS >**********************************************************************
|
||||
|
||||
/// Devuelve las clases CSS asociadas al panel.
|
||||
pub fn classes(&self) -> &AttrClasses {
|
||||
&self.classes
|
||||
}
|
||||
|
||||
/// Devuelve el título del panel.
|
||||
pub fn title(&self) -> &L10n {
|
||||
&self.title
|
||||
}
|
||||
|
||||
/// Devuelve el punto de ruptura configurado para cambiar el comportamiento del panel.
|
||||
pub fn breakpoint(&self) -> &BreakPoint {
|
||||
&self.breakpoint
|
||||
}
|
||||
|
||||
/// Devuelve el comportamiento configurado para la capa de fondo.
|
||||
pub fn backdrop(&self) -> &offcanvas::Backdrop {
|
||||
&self.backdrop
|
||||
}
|
||||
|
||||
/// Indica si la página principal puede desplazarse mientras el panel está abierto.
|
||||
pub fn body_scroll(&self) -> &offcanvas::BodyScroll {
|
||||
&self.scrolling
|
||||
}
|
||||
|
||||
/// Devuelve la posición de inicio del panel.
|
||||
pub fn placement(&self) -> &offcanvas::Placement {
|
||||
&self.placement
|
||||
}
|
||||
|
||||
/// Devuelve el estado inicial del panel.
|
||||
pub fn visibility(&self) -> &offcanvas::Visibility {
|
||||
&self.visibility
|
||||
}
|
||||
|
||||
/// Devuelve la lista de componentes (`children`) del panel.
|
||||
pub fn children(&self) -> &Children {
|
||||
&self.children
|
||||
}
|
||||
|
||||
// **< Offcanvas HELPERS >**********************************************************************
|
||||
|
||||
pub(crate) fn render_offcanvas(&self, cx: &mut Context, extra: Option<&Children>) -> Markup {
|
||||
|
|
@ -192,9 +152,9 @@ impl Offcanvas {
|
|||
return html! {};
|
||||
}
|
||||
|
||||
let id = cx.required_id::<Self>(self.id());
|
||||
let id_label = join!(id, "-label");
|
||||
let id_target = join!("#", id);
|
||||
let id = cx.required_id::<Self>(self.id(), 1);
|
||||
let id_label = util::join!(id, "-label");
|
||||
let id_target = util::join!("#", id);
|
||||
|
||||
let body_scroll = match self.body_scroll() {
|
||||
offcanvas::BodyScroll::Disabled => None,
|
||||
|
|
@ -211,7 +171,7 @@ impl Offcanvas {
|
|||
|
||||
html! {
|
||||
div
|
||||
id=(id)
|
||||
id=(&id)
|
||||
class=[self.classes().get()]
|
||||
tabindex="-1"
|
||||
data-bs-scroll=[body_scroll]
|
||||
|
|
@ -220,7 +180,7 @@ impl Offcanvas {
|
|||
{
|
||||
div class="offcanvas-header" {
|
||||
@if !title.is_empty() {
|
||||
h5 class="offcanvas-title" id=(id_label) { (title) }
|
||||
h5 id=(&id_label) class="offcanvas-title" { (title) }
|
||||
}
|
||||
button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ pub enum Placement {
|
|||
}
|
||||
|
||||
impl Placement {
|
||||
// Devuelve la clase base asociada a la posición de aparición del panel.
|
||||
/// Devuelve la clase base asociada a la posición de aparición del panel.
|
||||
#[rustfmt::skip]
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
|
|
@ -60,7 +60,7 @@ impl Placement {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada a la posición de aparición del panel a la cadena de clases.
|
||||
/// Añade la clase asociada a la posición de aparición del panel a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
if !classes.is_empty() {
|
||||
|
|
@ -89,7 +89,7 @@ pub enum Visibility {
|
|||
}
|
||||
|
||||
impl Visibility {
|
||||
// Devuelve la clase base asociada al estado inicial del panel.
|
||||
/// Devuelve la clase base asociada al estado inicial del panel.
|
||||
#[inline]
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
|
|
@ -98,7 +98,7 @@ impl Visibility {
|
|||
}
|
||||
}
|
||||
|
||||
// Añade la clase asociada al estado inicial del panel a la cadena de clases.
|
||||
/// Añade la clase asociada al estado inicial del panel a la cadena de clases.
|
||||
#[inline]
|
||||
pub(crate) fn push_class(self, classes: &mut String) {
|
||||
let class = self.as_str();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,58 @@ $enable-cssgrid: true;
|
|||
--bs-text-opacity: 0.1;
|
||||
}
|
||||
|
||||
// FORMS
|
||||
|
||||
// Required field indicator
|
||||
.form-required {
|
||||
color: var(--bs-danger);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
// Form fields
|
||||
.form-field {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Fieldset
|
||||
fieldset {
|
||||
position: relative;
|
||||
background-color: var(--bs-body-bg);
|
||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
padding: 2rem 1rem 1rem;
|
||||
margin: 2rem 0 1rem;
|
||||
}
|
||||
fieldset > legend {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 1rem;
|
||||
transform: translateY(-50%);
|
||||
background-color: var(--bs-body-bg);
|
||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
padding: 0.125rem 0.75rem;
|
||||
font-size: $font-size-sm;
|
||||
line-height: 1.25;
|
||||
width: fit-content;
|
||||
max-width: 75%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.fieldset-description {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
// Check buttons, gap between label and first inline check
|
||||
.form-label + .form-check-inline {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
// Extending utilities
|
||||
$utilities: map-merge(
|
||||
$utilities,
|
||||
|
|
@ -106,3 +158,9 @@ $utilities: map-merge(
|
|||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Region Footer
|
||||
.region-footer {
|
||||
padding: .75rem 0 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@
|
|||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
# ⚡️ Guía rápida
|
||||
## ⚡️ Guía rápida
|
||||
|
||||
Añadir en el archivo `Cargo.toml` del proyecto:
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ pagetop-build = { ... }
|
|||
Y crear un archivo `build.rs` a la altura de `Cargo.toml` para indicar cómo se van a incluir los
|
||||
archivos estáticos o cómo se van a compilar los archivos SCSS para el proyecto. Casos de uso:
|
||||
|
||||
## Incluir archivos estáticos desde un directorio
|
||||
### Incluir archivos estáticos desde un directorio
|
||||
|
||||
Hay que preparar una carpeta en el proyecto con todos los archivos que se quieren incluir, por
|
||||
ejemplo `static`, y añadir el siguiente código en `build.rs` para crear el conjunto de recursos:
|
||||
|
|
@ -64,7 +64,7 @@ fn main() -> std::io::Result<()> {
|
|||
}
|
||||
```
|
||||
|
||||
## Compilar archivos SCSS a CSS
|
||||
### Compilar archivos SCSS a CSS
|
||||
|
||||
Se puede compilar un archivo SCSS, que podría importar otros a su vez, para preparar un recurso con
|
||||
el archivo CSS minificado obtenido. Por ejemplo:
|
||||
|
|
@ -83,7 +83,7 @@ Este código compila el archivo `main.scss` de la carpeta `static` del proyecto,
|
|||
llamado `main_styles` que contiene el archivo `styles.min.css` obtenido.
|
||||
|
||||
|
||||
# 📦 Archivos generados
|
||||
## 📦 Archivos generados
|
||||
|
||||
Cada conjunto de recursos [`StaticFilesBundle`] genera un archivo en el directorio estándar
|
||||
[OUT_DIR](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts)
|
||||
|
|
@ -111,14 +111,14 @@ impl Extension for MyExtension {
|
|||
```
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ use pagetop::prelude::*;
|
|||
pub struct MyExtension;
|
||||
|
||||
impl Extension for MyExtension {
|
||||
// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
|
||||
/// Servicio web que publica los recursos de `guides` en `/ruta/a/guides`.
|
||||
fn configure_service(&self, scfg: &mut service::web::ServiceConfig) {
|
||||
static_files_service!(scfg, guides => "/ruta/a/guides");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,16 @@
|
|||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
## Créditos
|
||||
|
||||
Esta librería incluye entre sus macros una adaptación de
|
||||
## 📚 Créditos
|
||||
|
||||
Este *crate* incluye entre sus macros una adaptación de
|
||||
[maud-macros](https://crates.io/crates/maud_macros)
|
||||
([0.27.0](https://github.com/lambda-fairy/maud/tree/v0.27.0/maud_macros)) de
|
||||
[Chris Wong](https://crates.io/users/lambda-fairy) y una versión renombrada de
|
||||
|
|
@ -29,14 +30,14 @@ necesidad de referenciar `maud` o `smart_default` en las dependencias del archiv
|
|||
cada proyecto PageTop.
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ configurables, basadas en HTML, CSS y JavaScript.
|
|||
|
||||
## Créditos
|
||||
|
||||
Esta librería incluye entre sus macros una adaptación de
|
||||
Este *crate* incluye entre sus macros una adaptación de
|
||||
[maud-macros](https://crates.io/crates/maud_macros)
|
||||
([0.27.0](https://github.com/lambda-fairy/maud/tree/v0.27.0/maud_macros)) de
|
||||
[Chris Wong](https://crates.io/users/lambda-fairy) y una versión renombrada de
|
||||
|
|
@ -85,7 +85,7 @@ pub fn html(input: TokenStream) -> TokenStream {
|
|||
/// b: 0,
|
||||
/// c: Some(0),
|
||||
/// d: vec![1, 2, 3],
|
||||
/// e: "four".to_owned(),
|
||||
/// e: "four".to_string(),
|
||||
/// });
|
||||
/// # }
|
||||
/// ```
|
||||
|
|
@ -138,7 +138,7 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
|
|||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// la macro rescribirá el método `with_` y generará un nuevo método `alter_`:
|
||||
/// la macro reescribirá el método `with_` y generará un nuevo método `alter_`:
|
||||
///
|
||||
/// ```rust
|
||||
/// # struct Example {value: Option<String>};
|
||||
|
|
@ -157,7 +157,11 @@ pub fn derive_auto_default(input: TokenStream) -> TokenStream {
|
|||
/// ```
|
||||
///
|
||||
/// De esta forma, cada método *builder* `with_...()` generará automáticamente su correspondiente
|
||||
/// método `alter_...()` para dejar modificar instancias existentes.
|
||||
/// método `alter_...()` para modificar instancias existentes.
|
||||
///
|
||||
/// La documentación del método `with_...()` incluirá también la firma resumida del método
|
||||
/// `alter_...()` y un alias de búsqueda con su nombre, de tal manera que buscando `alter_...` en la
|
||||
/// documentación se mostrará la entrada del método `with_...()`.
|
||||
#[proc_macro_attribute]
|
||||
pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||
use syn::{parse2, FnArg, Ident, ImplItemFn, Pat, ReturnType, TraitItemFn, Type};
|
||||
|
|
@ -282,11 +286,11 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
// Genera el nombre del método alter_...().
|
||||
// Genera el nombre del método `alter_...()`.
|
||||
let stem = with_name_str.strip_prefix("with_").expect("validated");
|
||||
let alter_ident = Ident::new(&format!("alter_{stem}"), with_name.span());
|
||||
|
||||
// Extrae genéricos y cláusulas where.
|
||||
// Extrae genéricos y cláusulas `where`.
|
||||
let generics = &sig.generics;
|
||||
let where_clause = &sig.generics.where_clause;
|
||||
|
||||
|
|
@ -319,28 +323,75 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
v
|
||||
};
|
||||
|
||||
// Filtra los atributos descartando `#[doc]` y `#[inline]` para el método `alter_...()`.
|
||||
let non_doc_or_inline_attrs: Vec<_> = attrs
|
||||
.iter()
|
||||
.filter(|a| {
|
||||
let p = a.path();
|
||||
!p.is_ident("doc") && !p.is_ident("inline")
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
// Separa atributos de documentación y resto.
|
||||
let mut doc_attrs = Vec::new();
|
||||
let mut other_attrs = Vec::new();
|
||||
let mut non_doc_or_inline_attrs = Vec::new();
|
||||
|
||||
// Documentación del método alter_...().
|
||||
let doc = format!("Equivale a [`Self::{with_name_str}()`], pero fuera del patrón *builder*.");
|
||||
for a in attrs.iter() {
|
||||
let p = a.path();
|
||||
if p.is_ident("doc") {
|
||||
doc_attrs.push(a.clone());
|
||||
} else {
|
||||
other_attrs.push(a.clone());
|
||||
if !p.is_ident("inline") {
|
||||
non_doc_or_inline_attrs.push(a.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Firma resumida de la función `alter_...()` para mostrarla en la doc de `with_...()`.
|
||||
let alter_sig_tokens = if args.is_empty() {
|
||||
// Sin argumentos sólo se muestra `&mut self` (puede que no tenga mucho sentido).
|
||||
quote! { #vis_pub fn #alter_ident #generics (&mut self) -> &mut Self #where_clause }
|
||||
} else {
|
||||
// Con argumentos se muestra `&mut self, ...`.
|
||||
quote! { #vis_pub fn #alter_ident #generics (&mut self, ...) -> &mut Self #where_clause }
|
||||
};
|
||||
|
||||
// Normaliza espacios raros tipo `& mut`.
|
||||
let alter_sig_str = alter_sig_tokens.to_string().replace("& mut", "&mut");
|
||||
|
||||
// Nombre de la función `alter_...()` como alias de búsqueda.
|
||||
let alter_name_str = alter_ident.to_string();
|
||||
|
||||
// Texto introductorio para la documentación adicional de `with_...()`.
|
||||
let with_alter_title = format!(
|
||||
"# {} el método `{}()` generado por [`#[builder_fn]`](pagetop_macros::builder_fn)",
|
||||
if doc_attrs.is_empty() {
|
||||
"Añade"
|
||||
} else {
|
||||
"También añade"
|
||||
},
|
||||
alter_name_str
|
||||
);
|
||||
let with_alter_doc = concat!(
|
||||
"Permite modificar la instancia (`&mut self`) con los mismos argumentos ",
|
||||
"pero sin consumirla."
|
||||
);
|
||||
|
||||
// Atributos completos que se aplican siempre a `with_...()`.
|
||||
let with_prefix = quote! {
|
||||
#(#other_attrs)*
|
||||
#(#doc_attrs)*
|
||||
#[doc(alias = #alter_name_str)]
|
||||
#[doc = ""]
|
||||
#[doc = #with_alter_title]
|
||||
#[doc = #with_alter_doc]
|
||||
#[doc = "```text"]
|
||||
#[doc = #alter_sig_str]
|
||||
#[doc = "```"]
|
||||
};
|
||||
|
||||
// Genera el código final.
|
||||
let expanded = match body_opt {
|
||||
None => {
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
#with_prefix
|
||||
fn #with_name #generics (self, #(#args),*) -> Self #where_clause;
|
||||
|
||||
#(#non_doc_or_inline_attrs)*
|
||||
#[doc = #doc]
|
||||
#[doc(hidden)]
|
||||
fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause;
|
||||
}
|
||||
}
|
||||
|
|
@ -351,8 +402,10 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
} else {
|
||||
quote! { #[inline] }
|
||||
};
|
||||
|
||||
let with_fn = if is_trait {
|
||||
quote! {
|
||||
#with_prefix
|
||||
#force_inline
|
||||
#vis_pub fn #with_name #generics (self, #(#args),*) -> Self #where_clause {
|
||||
let mut s = self;
|
||||
|
|
@ -362,6 +415,7 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
}
|
||||
} else {
|
||||
quote! {
|
||||
#with_prefix
|
||||
#force_inline
|
||||
#vis_pub fn #with_name #generics (mut self, #(#args),*) -> Self #where_clause {
|
||||
self.#alter_ident(#(#call_idents),*);
|
||||
|
|
@ -369,12 +423,12 @@ pub fn builder_fn(_: TokenStream, item: TokenStream) -> TokenStream {
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
#with_fn
|
||||
|
||||
#(#non_doc_or_inline_attrs)*
|
||||
#[doc = #doc]
|
||||
#[doc(hidden)]
|
||||
#vis_pub fn #alter_ident #generics (&mut self, #(#args),*) -> &mut Self #where_clause {
|
||||
#body
|
||||
}
|
||||
|
|
|
|||
21
helpers/pagetop-minimal/Cargo.toml
Normal file
21
helpers/pagetop-minimal/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "pagetop-minimal"
|
||||
version = "0.0.10"
|
||||
edition = "2021"
|
||||
|
||||
description = """
|
||||
Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones
|
||||
básicas en PageTop.
|
||||
"""
|
||||
categories = ["development-tools::build-utils"]
|
||||
keywords = ["pagetop", "build", "assets", "resources", "static"]
|
||||
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
concat-string = "1.0"
|
||||
indoc = "2.0"
|
||||
pastey = "0.2"
|
||||
201
helpers/pagetop-minimal/LICENSE-APACHE
Normal file
201
helpers/pagetop-minimal/LICENSE-APACHE
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022 Manuel Cillero
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
21
helpers/pagetop-minimal/LICENSE-MIT
Normal file
21
helpers/pagetop-minimal/LICENSE-MIT
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Manuel Cillero
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
61
helpers/pagetop-minimal/README.md
Normal file
61
helpers/pagetop-minimal/README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<div align="center">
|
||||
|
||||
<h1>PageTop Minimal</h1>
|
||||
|
||||
<p>Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones básicas en <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-minimal)
|
||||
[](https://crates.io/crates/pagetop-minimal)
|
||||
[](https://crates.io/crates/pagetop-minimal)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal#licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
|
||||
## 🗺️ Descripción general
|
||||
|
||||
Este *crate* proporciona un conjunto básico de macros que se integran en las utilidades de PageTop
|
||||
para optimizar operaciones habituales relacionadas con la composición estructurada de texto, la
|
||||
concatenación de cadenas y el uso rápido de colecciones clave-valor.
|
||||
|
||||
|
||||
## 📚 Créditos
|
||||
|
||||
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
|
||||
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
|
||||
|
||||
Las macros para la concatenación de cadenas **`join!`** y **`join_pair!`** se apoyan internamente en
|
||||
el *crate* [concat-string](https://crates.io/crates/concat_string), desarrollado por
|
||||
[FaultyRAM](https://crates.io/users/FaultyRAM), para evitar el formato de cadenas cuando la
|
||||
eficiencia pueda ser relevante.
|
||||
|
||||
La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate*
|
||||
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
|
||||
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
|
||||
|
||||
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
* **Licencia MIT**
|
||||
([LICENSE-MIT](LICENSE-MIT) o también https://opensource.org/licenses/MIT)
|
||||
|
||||
* **Licencia Apache, Versión 2.0**
|
||||
([LICENSE-APACHE](LICENSE-APACHE) o también https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Puedes elegir la licencia que prefieras. Este enfoque de doble licencia es el estándar de facto en
|
||||
el ecosistema Rust.
|
||||
161
helpers/pagetop-minimal/src/lib.rs
Normal file
161
helpers/pagetop-minimal/src/lib.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/*!
|
||||
<div align="center">
|
||||
|
||||
<h1>PageTop Minimal</h1>
|
||||
|
||||
<p>Reúne un conjunto mínimo de macros para mejorar el formato y la eficiencia de operaciones básicas en <strong>PageTop</strong>.</p>
|
||||
|
||||
[](https://docs.rs/pagetop-minimal)
|
||||
[](https://crates.io/crates/pagetop-minimal)
|
||||
[](https://crates.io/crates/pagetop-minimal)
|
||||
[](https://git.cillero.es/manuelcillero/pagetop/src/branch/main/helpers/pagetop-minimal#licencia)
|
||||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
## Descripción general
|
||||
|
||||
Este *crate* proporciona un conjunto básico de macros que se integran en las utilidades de PageTop
|
||||
para optimizar operaciones habituales relacionadas con la composición estructurada de texto, la
|
||||
concatenación de cadenas y el uso rápido de colecciones clave-valor.
|
||||
|
||||
## Créditos
|
||||
|
||||
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
|
||||
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
|
||||
|
||||
Las macros para la concatenación de cadenas **`join!`** y **`join_pair!`** se apoyan internamente en
|
||||
el *crate* [concat-string](https://crates.io/crates/concat_string), desarrollado por
|
||||
[FaultyRAM](https://crates.io/users/FaultyRAM), para evitar el formato de cadenas cuando la
|
||||
eficiencia pueda ser relevante.
|
||||
|
||||
La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate*
|
||||
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
|
||||
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
|
||||
*/
|
||||
|
||||
#![doc(
|
||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
|
||||
)]
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use concat_string::concat_string;
|
||||
|
||||
pub use indoc::{concatdoc, formatdoc, indoc};
|
||||
|
||||
/// Permite *pegar* tokens y generar identificadores a partir de otros.
|
||||
///
|
||||
/// Dentro de `paste!`, los identificadores escritos como `[< ... >]` se combinan en uno solo que
|
||||
/// puede reutilizarse para referirse a items existentes o para definir nuevos (funciones,
|
||||
/// estructuras, métodos, etc.).
|
||||
///
|
||||
/// También admite modificadores de estilo (`lower`, `upper`, `snake`, `camel`, etc.) para
|
||||
/// transformar fragmentos interpolados antes de construir el nuevo identificador.
|
||||
pub use pastey::paste;
|
||||
// La documentación anterior se copia en `pagetop::util::paste!` porque el *crate* original no la
|
||||
// define y `pagetop` no la hereda automáticamente.
|
||||
|
||||
/// Concatena eficientemente varios fragmentos en un [`String`].
|
||||
///
|
||||
/// Esta macro exporta [`concat_string!`](https://docs.rs/concat-string). Acepta cualquier número de
|
||||
/// fragmentos que implementen [`AsRef<str>`] y construye un [`String`] con el tamaño óptimo, de
|
||||
/// forma eficiente y evitando el uso de cadenas de formato que penalicen el rendimiento.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_minimal::join;
|
||||
/// // Concatena todos los fragmentos directamente.
|
||||
/// let result = join!("Hello", " ", "World");
|
||||
/// assert_eq!(result, "Hello World".to_string());
|
||||
///
|
||||
/// // También funciona con valores vacíos.
|
||||
/// let result_with_empty = join!("Hello", "", "World");
|
||||
/// assert_eq!(result_with_empty, "HelloWorld".to_string());
|
||||
///
|
||||
/// // Un único fragmento devuelve el mismo valor.
|
||||
/// let single_result = join!("Hello");
|
||||
/// assert_eq!(single_result, "Hello".to_string());
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! join {
|
||||
($($arg:expr),+) => {
|
||||
$crate::concat_string!($($arg),+)
|
||||
};
|
||||
}
|
||||
|
||||
/// Concatena dos fragmentos en un [`String`] usando un separador inteligente.
|
||||
///
|
||||
/// Une los dos fragmentos, que deben implementar [`AsRef<str>`], usando el separador proporcionado.
|
||||
/// Si uno de ellos está vacío, devuelve directamente el otro; y si ambos están vacíos devuelve un
|
||||
/// [`String`] vacío.
|
||||
///
|
||||
/// # Ejemplo
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_minimal::join_pair;
|
||||
/// let first = "Hello";
|
||||
/// let separator = "-";
|
||||
/// let second = "World";
|
||||
///
|
||||
/// // Concatena los dos fragmentos cuando ambos no están vacíos.
|
||||
/// let result = join_pair!(first, separator, second);
|
||||
/// assert_eq!(result, "Hello-World".to_string());
|
||||
///
|
||||
/// // Si el primer fragmento está vacío, devuelve el segundo.
|
||||
/// let result_empty_first = join_pair!("", separator, second);
|
||||
/// assert_eq!(result_empty_first, "World".to_string());
|
||||
///
|
||||
/// // Si el segundo fragmento está vacío, devuelve el primero.
|
||||
/// let result_empty_second = join_pair!(first, separator, "");
|
||||
/// assert_eq!(result_empty_second, "Hello".to_string());
|
||||
///
|
||||
/// // Si ambos fragmentos están vacíos, devuelve una cadena vacía.
|
||||
/// let result_both_empty = join_pair!("", separator, "");
|
||||
/// assert_eq!(result_both_empty, "".to_string());
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! join_pair {
|
||||
($first:expr, $separator:expr, $second:expr) => {{
|
||||
let first_val = $first;
|
||||
let second_val = $second;
|
||||
let separator_val = $separator;
|
||||
|
||||
let first = AsRef::<str>::as_ref(&first_val);
|
||||
let second = AsRef::<str>::as_ref(&second_val);
|
||||
let separator = if first.is_empty() || second.is_empty() {
|
||||
""
|
||||
} else {
|
||||
AsRef::<str>::as_ref(&separator_val)
|
||||
};
|
||||
|
||||
$crate::concat_string!(first, separator, second)
|
||||
}};
|
||||
}
|
||||
|
||||
/// Macro para construir una colección de pares clave-valor.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use pagetop_minimal::kv;
|
||||
/// # use std::collections::HashMap;
|
||||
/// let args:HashMap<&str, String> = kv![
|
||||
/// "userName" => "Roberto",
|
||||
/// "photoCount" => "3",
|
||||
/// "userGender" => "male",
|
||||
/// ];
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! kv {
|
||||
( $($key:expr => $value:expr),* $(,)? ) => {{
|
||||
let mut a = std::collections::HashMap::new();
|
||||
$(
|
||||
a.insert($key.into(), $value.into());
|
||||
)*
|
||||
a
|
||||
}};
|
||||
}
|
||||
|
|
@ -11,19 +11,21 @@
|
|||
|
||||
</div>
|
||||
|
||||
## Sobre PageTop
|
||||
## 🧭 Sobre PageTop
|
||||
|
||||
[PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la web
|
||||
clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles y
|
||||
configurables, basadas en HTML, CSS y JavaScript.
|
||||
|
||||
## Descripción general
|
||||
|
||||
Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
## 🗺️ Descripción general
|
||||
|
||||
Este *crate* permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
|
||||
## Créditos
|
||||
|
||||
## 📚 Créditos
|
||||
|
||||
Para ello, adapta el código de los *crates* [static-files](https://crates.io/crates/static_files)
|
||||
(versión [0.2.5](https://github.com/static-files-rs/static-files/tree/v0.2.5)) y
|
||||
|
|
@ -35,14 +37,14 @@ Estas implementaciones se integran en PageTop para evitar que cada proyecto teng
|
|||
`static-files` manualmente como dependencia en su `Cargo.toml`.
|
||||
|
||||
|
||||
# 🚧 Advertencia
|
||||
## 🚧 Advertencia
|
||||
|
||||
**PageTop** es un proyecto personal para aprender [Rust](https://www.rust-lang.org/es) y conocer su
|
||||
ecosistema. Su API está sujeta a cambios frecuentes. No se recomienda su uso en producción, al menos
|
||||
hasta que se libere la versión **1.0.0**.
|
||||
|
||||
|
||||
# 📜 Licencia
|
||||
## 📜 Licencia
|
||||
|
||||
El código está disponible bajo una doble licencia:
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ configurables, basadas en HTML, CSS y JavaScript.
|
|||
|
||||
## Descripción general
|
||||
|
||||
Esta librería permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
Este *crate* permite incluir archivos estáticos en el ejecutable de las aplicaciones PageTop para
|
||||
servirlos de forma eficiente vía web, con detección de cambios que optimizan el tiempo de
|
||||
compilación.
|
||||
|
||||
|
|
|
|||
17
src/app.rs
17
src/app.rs
|
|
@ -4,9 +4,10 @@ mod figfont;
|
|||
|
||||
use crate::core::{extension, extension::ExtensionRef};
|
||||
use crate::html::Markup;
|
||||
use crate::locale::Locale;
|
||||
use crate::response::page::{ErrorPage, ResultPage};
|
||||
use crate::service::HttpRequest;
|
||||
use crate::{global, locale, service, trace};
|
||||
use crate::{global, service, trace, PAGETOP_VERSION};
|
||||
|
||||
use actix_session::config::{BrowserSession, PersistentSession, SessionLifecycle};
|
||||
use actix_session::storage::CookieSessionStore;
|
||||
|
|
@ -49,7 +50,7 @@ impl Application {
|
|||
Self::internal_prepare(Some(root_extension))
|
||||
}
|
||||
|
||||
// Método interno para preparar la aplicación, opcionalmente con una extensión.
|
||||
/// Método interno para preparar la aplicación, opcionalmente con una extensión.
|
||||
fn internal_prepare(root_extension: Option<ExtensionRef>) -> Self {
|
||||
// Al arrancar muestra una cabecera para la aplicación.
|
||||
Self::show_banner();
|
||||
|
|
@ -57,8 +58,8 @@ impl Application {
|
|||
// Inicia gestión de trazas y registro de eventos (logging).
|
||||
LazyLock::force(&trace::TRACING);
|
||||
|
||||
// Valida el identificador de idioma por defecto.
|
||||
LazyLock::force(&locale::DEFAULT_LANGID);
|
||||
// Inicializa el idioma predeterminado.
|
||||
Locale::init();
|
||||
|
||||
// Registra las extensiones de la aplicación.
|
||||
extension::all::register_extensions(root_extension);
|
||||
|
|
@ -72,12 +73,12 @@ impl Application {
|
|||
Self
|
||||
}
|
||||
|
||||
// Muestra una cabecera para la aplicación basada en la configuración.
|
||||
/// Muestra una cabecera para la aplicación basada en la configuración.
|
||||
fn show_banner() {
|
||||
use colored::Colorize;
|
||||
use terminal_size::{terminal_size, Width};
|
||||
|
||||
if global::SETTINGS.app.startup_banner.to_lowercase() != "off" {
|
||||
if global::SETTINGS.app.startup_banner != global::StartupBanner::Off {
|
||||
// Nombre de la aplicación, ajustado al ancho del terminal si es necesario.
|
||||
let mut app_ff = String::new();
|
||||
let app_name = &global::SETTINGS.app.name;
|
||||
|
|
@ -108,7 +109,7 @@ impl Application {
|
|||
println!(
|
||||
"{} {}\n",
|
||||
"Powered by PageTop".yellow(),
|
||||
env!("CARGO_PKG_VERSION").yellow()
|
||||
PAGETOP_VERSION.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -163,7 +164,7 @@ impl Application {
|
|||
Self::service_app()
|
||||
}
|
||||
|
||||
// Configura el servicio web de la aplicación.
|
||||
/// Configura el servicio web de la aplicación.
|
||||
fn service_app() -> service::App<
|
||||
impl service::Factory<
|
||||
service::Request,
|
||||
|
|
|
|||
|
|
@ -2,29 +2,19 @@ use crate::global;
|
|||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use figlet_rs::FIGfont;
|
||||
use figlet_rs::FIGlet;
|
||||
|
||||
pub static FIGFONT: LazyLock<FIGfont> = LazyLock::new(|| {
|
||||
pub static FIGFONT: LazyLock<FIGlet> = LazyLock::new(|| {
|
||||
let slant = include_str!("slant.flf");
|
||||
let small = include_str!("small.flf");
|
||||
let speed = include_str!("speed.flf");
|
||||
let starwars = include_str!("starwars.flf");
|
||||
|
||||
FIGfont::from_content(
|
||||
match global::SETTINGS.app.startup_banner.to_lowercase().as_str() {
|
||||
"off" => slant,
|
||||
"slant" => slant,
|
||||
"small" => small,
|
||||
"speed" => speed,
|
||||
"starwars" => starwars,
|
||||
_ => {
|
||||
println!(
|
||||
"\n FIGfont \"{}\" not found for banner. Using \"Slant\". Check settings.",
|
||||
global::SETTINGS.app.startup_banner,
|
||||
);
|
||||
slant
|
||||
}
|
||||
},
|
||||
)
|
||||
FIGlet::from_content(match global::SETTINGS.app.startup_banner {
|
||||
global::StartupBanner::Off | global::StartupBanner::Slant => slant,
|
||||
global::StartupBanner::Small => small,
|
||||
global::StartupBanner::Speed => speed,
|
||||
global::StartupBanner::Starwars => starwars,
|
||||
})
|
||||
.unwrap()
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,5 @@
|
|||
//! Acciones predefinidas para alterar el funcionamiento interno de PageTop.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// Tipo de función para manipular componentes y su contexto de renderizado.
|
||||
///
|
||||
/// Se usa en acciones definidas en [`component`] y [`theme`] para alterar el comportamiento de los
|
||||
/// componentes.
|
||||
///
|
||||
/// Recibe referencias mutables (`&mut`) del componente `component` y del contexto `cx`.
|
||||
pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
|
||||
|
||||
pub mod component;
|
||||
|
||||
pub mod theme;
|
||||
|
||||
pub mod page;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,35 @@
|
|||
//! Acciones que operan sobre componentes.
|
||||
|
||||
use pagetop::prelude::*;
|
||||
|
||||
/// Tipo de función para manipular componentes y su contexto de renderizado.
|
||||
///
|
||||
/// Se usa en [`action::component::BeforeRender`] y [`action::component::AfterRender`] para alterar
|
||||
/// el comportamiento predefinido de los componentes antes y después de renderizarlos.
|
||||
///
|
||||
/// Recibe referencias mutables del componente `component` y del contexto `cx`.
|
||||
pub type FnActionWithComponent<C> = fn(component: &mut C, cx: &mut Context);
|
||||
|
||||
/// Tipo de función para alterar el [`Markup`] generado por un componente.
|
||||
///
|
||||
/// Se usa en [`action::component::TransformMarkup`] para permitir a las extensiones alterar el HTML
|
||||
/// final producido por el renderizado de un componente. La edición trabaja a nivel de texto: el
|
||||
/// [`Markup`] recibido expone su contenido como [`String`], lo que permite aplicar búsquedas,
|
||||
/// sustituciones, concatenaciones y cualquier otra primitiva de trabajo con cadenas.
|
||||
///
|
||||
/// La función recibe una referencia inmutable al componente `component` (el renderizado ya ha
|
||||
/// concluido, solo se necesita leer su estado), y al contexto `cx` (solo para consulta), y toma
|
||||
/// posesión del `markup` producido hasta ese momento.
|
||||
///
|
||||
/// Devuelve el nuevo [`Markup`] transformado, que se encadena como entrada para la siguiente acción
|
||||
/// registrada, si la hay.
|
||||
pub type FnActionTransformMarkup<C> = fn(component: &C, cx: &Context, markup: Markup) -> Markup;
|
||||
|
||||
mod before_render_component;
|
||||
pub use before_render_component::*;
|
||||
|
||||
mod after_render_component;
|
||||
pub use after_render_component::*;
|
||||
|
||||
mod transform_markup_component;
|
||||
pub use transform_markup_component::*;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use crate::base::action::FnActionWithComponent;
|
||||
use super::FnActionWithComponent;
|
||||
|
||||
/// Ejecuta [`FnActionWithComponent`] después de renderizar un componente.
|
||||
pub struct AfterRender<C: Component> {
|
||||
|
|
@ -42,7 +42,7 @@ impl<C: Component> AfterRender<C> {
|
|||
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
|
||||
/// `C` con identificador `id`.
|
||||
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.referer_id.alter_value(id);
|
||||
self.referer_id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -52,28 +52,19 @@ impl<C: Component> AfterRender<C> {
|
|||
self
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
|
||||
// Primero despacha las acciones para el tipo de componente.
|
||||
dispatch_actions(
|
||||
&ActionKey::new(
|
||||
UniqueId::of::<Self>(),
|
||||
None,
|
||||
Some(UniqueId::of::<C>()),
|
||||
None,
|
||||
),
|
||||
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
|
||||
|action: &Self| (action.f)(component, cx),
|
||||
);
|
||||
|
||||
// Y luego despacha las acciones para el tipo de componente con un identificador dado.
|
||||
if let Some(id) = component.id() {
|
||||
dispatch_actions(
|
||||
&ActionKey::new(
|
||||
UniqueId::of::<Self>(),
|
||||
None,
|
||||
Some(UniqueId::of::<C>()),
|
||||
Some(id),
|
||||
),
|
||||
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
|
||||
|action: &Self| (action.f)(component, cx),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use crate::base::action::FnActionWithComponent;
|
||||
use super::FnActionWithComponent;
|
||||
|
||||
/// Ejecuta [`FnActionWithComponent`] antes de renderizar el componente.
|
||||
pub struct BeforeRender<C: Component> {
|
||||
|
|
@ -42,7 +42,7 @@ impl<C: Component> BeforeRender<C> {
|
|||
/// Afina el registro para ejecutar la acción [`FnActionWithComponent`] sólo para el componente
|
||||
/// `C` con identificador `id`.
|
||||
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.referer_id.alter_value(id);
|
||||
self.referer_id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -52,28 +52,19 @@ impl<C: Component> BeforeRender<C> {
|
|||
self
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
|
||||
// Primero despacha las acciones para el tipo de componente.
|
||||
dispatch_actions(
|
||||
&ActionKey::new(
|
||||
UniqueId::of::<Self>(),
|
||||
None,
|
||||
Some(UniqueId::of::<C>()),
|
||||
None,
|
||||
),
|
||||
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
|
||||
|action: &Self| (action.f)(component, cx),
|
||||
);
|
||||
|
||||
// Y luego despacha las aciones para el tipo de componente con un identificador dado.
|
||||
if let Some(id) = component.id() {
|
||||
dispatch_actions(
|
||||
&ActionKey::new(
|
||||
UniqueId::of::<Self>(),
|
||||
None,
|
||||
Some(UniqueId::of::<C>()),
|
||||
Some(id),
|
||||
),
|
||||
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
|
||||
|action: &Self| (action.f)(component, cx),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
82
src/base/action/component/transform_markup_component.rs
Normal file
82
src/base/action/component/transform_markup_component.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use super::FnActionTransformMarkup;
|
||||
|
||||
/// Ejecuta [`FnActionTransformMarkup`] para alterar el renderizado de componentes.
|
||||
pub struct TransformMarkup<C: Component> {
|
||||
f: FnActionTransformMarkup<C>,
|
||||
referer_type_id: Option<UniqueId>,
|
||||
referer_id: AttrId,
|
||||
weight: Weight,
|
||||
}
|
||||
|
||||
/// Filtro para despachar [`FnActionTransformMarkup`] sobre el renderizado de un componente `C`.
|
||||
impl<C: Component> ActionDispatcher for TransformMarkup<C> {
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
|
||||
fn referer_type_id(&self) -> Option<UniqueId> {
|
||||
self.referer_type_id
|
||||
}
|
||||
|
||||
/// Devuelve el identificador del componente.
|
||||
fn referer_id(&self) -> Option<String> {
|
||||
self.referer_id.get()
|
||||
}
|
||||
|
||||
/// Devuelve el peso para definir el orden de ejecución.
|
||||
fn weight(&self) -> Weight {
|
||||
self.weight
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> TransformMarkup<C> {
|
||||
/// Permite [registrar](Extension::actions) una nueva acción [`FnActionTransformMarkup`].
|
||||
pub fn new(f: FnActionTransformMarkup<C>) -> Self {
|
||||
TransformMarkup {
|
||||
f,
|
||||
referer_type_id: Some(UniqueId::of::<C>()),
|
||||
referer_id: AttrId::default(),
|
||||
weight: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Afina el registro para ejecutar la acción [`FnActionTransformMarkup`] sólo para el
|
||||
/// componente `C` con identificador `id`.
|
||||
pub fn filter_by_referer_id(mut self, id: impl AsRef<str>) -> Self {
|
||||
self.referer_id.alter_id(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Opcional. Acciones con pesos más bajos se aplican antes. Se pueden usar valores negativos.
|
||||
pub fn with_weight(mut self, value: Weight) -> Self {
|
||||
self.weight = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Despacha las acciones encadenando el [`Markup`] entre cada una.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &C, cx: &Context, markup: Markup) -> Markup {
|
||||
let mut output = markup;
|
||||
|
||||
// Primero despacha las acciones para el tipo de componente.
|
||||
dispatch_actions(
|
||||
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), None),
|
||||
|action: &Self| {
|
||||
let taken = std::mem::replace(&mut output, html! {});
|
||||
output = (action.f)(component, cx, taken);
|
||||
},
|
||||
);
|
||||
|
||||
// Y luego despacha las acciones para el tipo de componente con un identificador dado.
|
||||
if let Some(id) = component.id() {
|
||||
dispatch_actions(
|
||||
&ActionKey::new(UniqueId::of::<Self>(), Some(UniqueId::of::<C>()), Some(id)),
|
||||
|action: &Self| {
|
||||
let taken = std::mem::replace(&mut output, html! {});
|
||||
output = (action.f)(component, cx, taken);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use crate::base::action::page::FnActionWithPage;
|
||||
use super::FnActionWithPage;
|
||||
|
||||
/// Ejecuta [`FnActionWithPage`](crate::base::action::page::FnActionWithPage) después de renderizar
|
||||
/// el cuerpo de la página.
|
||||
|
|
@ -34,12 +34,12 @@ impl AfterRenderBody {
|
|||
self
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline(always)]
|
||||
#[allow(clippy::inline_always)]
|
||||
pub(crate) fn dispatch(page: &mut Page) {
|
||||
dispatch_actions(
|
||||
&ActionKey::new(UniqueId::of::<Self>(), None, None, None),
|
||||
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|
||||
|action: &Self| (action.f)(page),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use crate::base::action::page::FnActionWithPage;
|
||||
use super::FnActionWithPage;
|
||||
|
||||
/// Ejecuta [`FnActionWithPage`](crate::base::action::page::FnActionWithPage) antes de renderizar
|
||||
/// el cuerpo de la página.
|
||||
|
|
@ -34,12 +34,12 @@ impl BeforeRenderBody {
|
|||
self
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
/// Despacha las acciones.
|
||||
#[inline(always)]
|
||||
#[allow(clippy::inline_always)]
|
||||
pub(crate) fn dispatch(page: &mut Page) {
|
||||
dispatch_actions(
|
||||
&ActionKey::new(UniqueId::of::<Self>(), None, None, None),
|
||||
&ActionKey::new(UniqueId::of::<Self>(), None, None),
|
||||
|action: &Self| (action.f)(page),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
//! Acciones lanzadas desde los temas.
|
||||
|
||||
mod before_render_component;
|
||||
pub use before_render_component::*;
|
||||
|
||||
mod after_render_component;
|
||||
pub use after_render_component::*;
|
||||
|
||||
mod prepare_render;
|
||||
pub use prepare_render::*;
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use crate::base::action::FnActionWithComponent;
|
||||
|
||||
/// Ejecuta [`FnActionWithComponent`] después de que un tema renderice el componente.
|
||||
pub struct AfterRender<C: Component> {
|
||||
f: FnActionWithComponent<C>,
|
||||
theme_type_id: Option<UniqueId>,
|
||||
referer_type_id: Option<UniqueId>,
|
||||
}
|
||||
|
||||
/// Filtro para despachar [`FnActionWithComponent`] después de que un tema renderice el componente
|
||||
/// `C`.
|
||||
impl<C: Component> ActionDispatcher for AfterRender<C> {
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del tema.
|
||||
fn theme_type_id(&self) -> Option<UniqueId> {
|
||||
self.theme_type_id
|
||||
}
|
||||
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
|
||||
fn referer_type_id(&self) -> Option<UniqueId> {
|
||||
self.referer_type_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> AfterRender<C> {
|
||||
/// Permite [registrar](Extension::actions) una nueva acción [`FnActionWithComponent`] para un
|
||||
/// tema dado.
|
||||
pub fn new(theme: ThemeRef, f: FnActionWithComponent<C>) -> Self {
|
||||
AfterRender {
|
||||
f,
|
||||
theme_type_id: Some(theme.type_id()),
|
||||
referer_type_id: Some(UniqueId::of::<C>()),
|
||||
}
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
|
||||
dispatch_actions(
|
||||
&ActionKey::new(
|
||||
UniqueId::of::<Self>(),
|
||||
Some(cx.theme().type_id()),
|
||||
Some(UniqueId::of::<C>()),
|
||||
None,
|
||||
),
|
||||
|action: &Self| (action.f)(component, cx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
use crate::base::action::FnActionWithComponent;
|
||||
|
||||
/// Ejecuta [`FnActionWithComponent`] antes de que un tema renderice el componente.
|
||||
pub struct BeforeRender<C: Component> {
|
||||
f: FnActionWithComponent<C>,
|
||||
theme_type_id: Option<UniqueId>,
|
||||
referer_type_id: Option<UniqueId>,
|
||||
}
|
||||
|
||||
/// Filtro para despachar [`FnActionWithComponent`] antes de que un tema renderice el componente
|
||||
/// `C`.
|
||||
impl<C: Component> ActionDispatcher for BeforeRender<C> {
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del tema.
|
||||
fn theme_type_id(&self) -> Option<UniqueId> {
|
||||
self.theme_type_id
|
||||
}
|
||||
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
|
||||
fn referer_type_id(&self) -> Option<UniqueId> {
|
||||
self.referer_type_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> BeforeRender<C> {
|
||||
/// Permite [registrar](Extension::actions) una nueva acción [`FnActionWithComponent`] para un
|
||||
/// tema dado.
|
||||
pub fn new(theme: ThemeRef, f: FnActionWithComponent<C>) -> Self {
|
||||
BeforeRender {
|
||||
f,
|
||||
theme_type_id: Some(theme.type_id()),
|
||||
referer_type_id: Some(UniqueId::of::<C>()),
|
||||
}
|
||||
}
|
||||
|
||||
// Despacha las acciones.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &mut C, cx: &mut Context) {
|
||||
dispatch_actions(
|
||||
&ActionKey::new(
|
||||
UniqueId::of::<Self>(),
|
||||
Some(cx.theme().type_id()),
|
||||
Some(UniqueId::of::<C>()),
|
||||
None,
|
||||
),
|
||||
|action: &Self| (action.f)(component, cx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// Tipo de función para alterar el renderizado de un componente.
|
||||
///
|
||||
/// Permite a un [tema](crate::base::action::theme) sobreescribir el renderizado predeterminado de
|
||||
/// los componentes.
|
||||
///
|
||||
/// Recibe una referencia al componente `component` y una referencia mutable al contexto `cx`.
|
||||
pub type FnPrepareRender<C> = fn(component: &C, cx: &mut Context) -> PrepareMarkup;
|
||||
|
||||
/// Ejecuta [`FnPrepareRender`] para preparar el renderizado de un componente.
|
||||
///
|
||||
/// Permite a un tema hacer una implementación nueva del renderizado de un componente.
|
||||
pub struct PrepareRender<C: Component> {
|
||||
f: FnPrepareRender<C>,
|
||||
theme_type_id: Option<UniqueId>,
|
||||
referer_type_id: Option<UniqueId>,
|
||||
}
|
||||
|
||||
/// Filtro para despachar [`FnPrepareRender`] que modifica el renderizado de un componente `C`.
|
||||
impl<C: Component> ActionDispatcher for PrepareRender<C> {
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del tema.
|
||||
fn theme_type_id(&self) -> Option<UniqueId> {
|
||||
self.theme_type_id
|
||||
}
|
||||
|
||||
/// Devuelve el identificador de tipo ([`UniqueId`]) del componente `C`.
|
||||
fn referer_type_id(&self) -> Option<UniqueId> {
|
||||
self.referer_type_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Component> PrepareRender<C> {
|
||||
/// Permite [registrar](Extension::actions) una nueva acción [`FnPrepareRender`] para un tema
|
||||
/// dado.
|
||||
pub fn new(theme: ThemeRef, f: FnPrepareRender<C>) -> Self {
|
||||
PrepareRender {
|
||||
f,
|
||||
theme_type_id: Some(theme.type_id()),
|
||||
referer_type_id: Some(UniqueId::of::<C>()),
|
||||
}
|
||||
}
|
||||
|
||||
// Despacha las acciones. Se detiene en cuanto una renderiza.
|
||||
#[inline]
|
||||
pub(crate) fn dispatch(component: &C, cx: &mut Context) -> PrepareMarkup {
|
||||
let mut render_component = PrepareMarkup::None;
|
||||
dispatch_actions(
|
||||
&ActionKey::new(
|
||||
UniqueId::of::<Self>(),
|
||||
Some(cx.theme().type_id()),
|
||||
Some(UniqueId::of::<C>()),
|
||||
None,
|
||||
),
|
||||
|action: &Self| {
|
||||
if render_component.is_empty() {
|
||||
render_component = (action.f)(component, cx);
|
||||
}
|
||||
},
|
||||
);
|
||||
render_component
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue