por Gilberto Pacheco Gallegos
Este curso trata sobre PWA, que permite crear aplicaciones web de alto rendimiento, que se pueden instalar y comportarse com app casi nativas.
Utiliza el botón para mostrar el menú de navegación, o el enlace skip_next para avanzar a las siguietes paginas. La página Instrucciones de navegación te muestra otras opciones para desplazarte por el sitio.
Puedes ver el listado de mis sitios educativos en https://gilpgcl.github.io.
Este sitio es continuación de https://gilpgiti.github.io, https://gilpgijs.github.io, https://gilpgihc.github.io, https://gilpgawoas.github.io, https://gilpgdam.github.io y https://gilpgad.github.io
El contenido sobre pruebas se ha desplazado a https://gilpgawoas.github.io
El contenido sobre multmedia se ha desplazado a https://gilpgihc.github.io
El contenido sobre aplicaciones nativas y multiplataforma se ha desplazado a https://gilpgiti.github.io
Los siguientes controles te permitirán navegar por todo el contenido del sitio.
Oculta el menú de navegación.
Muestra el menú de navegación.
Regresa a la página anterior.
Avanza a la página siguiente.
Ya viene instalado en las computadoras más nuevas, pero si la tuya no lo tiene, lo puedes descargar de https://www.microsoft.com/es-es/edge.
Lo puedes descargar de https://www.mozilla.org.
Lo puedes descargar de https://google.com/chrome.
Se puede instalar desde las tiendas de software.
Descarga la versión Thread Safe que corresponda a la arquitectura de tu computadora (x64 o x86, la maypría son x64) desde https://windows.php.net/download#php-8.3.
Crea una carpeta con nombre php en el disco C: y descompacta ahí el php
Crea el archivo C:\php\php.ini con el siguiente contenido
extension = php_mbstring.dll
extension=php_pdo_sqlite.dll
extension=php_pdo_mysql.dll
extension=php_openssl.dll
extension=php_sockets.dll
extension=php_fileinfo.dll
zend_extension = xdebug
xdebug.mode = debug
xdebug.start_with_request = yes
Añade la ruta C:\php a la variable de ambiente PATH.
Haz clic en el logo de Windows.
Haz clic en Configuración.
Haz clic en Sistema.
Haz clic en Acerca de.
Haz clic en Configuración avanzada del sistema. Te pide que autorices. Autoriza.
Haz clic en Variables de entorno.
En Variables del sistema selecciona Path y haz clic en Editar...
Haz clic en Nuevo. Se abre una entrada y en ella teclea C:\php. Haz clic en Aceptar.
Haz clic en Aceptar para cerrar las variables de entorno.
Haz clic en Aceptar para cerrar las propiedades del sistema.
Usa la URL https://code.visualstudio.com/.
Instala las extensiones:
Haz clic en el engrane de abajo a la izquierda y seleciona Settings. En Extensions > PHP Server configuration configura lo siguiente
Crea una carpeta vacía y ábrela con Visual Studio Code.
Crea el archivo index.php con el siguiente contenido
<?php
phpinfo();
Haz clic derecho en index.php y selecciona PHP Server: serve project
Sigue las instrucciones de https://xdebug.org/wizard
Lo puedes descargar de https://www.silabs.com/developers/usb-to-uart-bridge-vcp-drivers.
Las instrucciones de instalación son los siguientes:
Selecciona la pestaña DOWNLOADS y descarga el archivo
CP210x Universal Windows Driver
.
Descompácta el archivo, haz clic derecho en
silabser.inf
y selecciona instalar.
Puedes descargar Arduino IDE desde https://www.arduino.cc/.
Video de Instalación de Arduino IDE + ESP8266 + ESP32 + ArduinoWebSockets.
En el video no se muestra la instalación de la libresría EspMQTTClient, pero se instala como una librería normal. Si te solicita instalar otras librerías, instálalas también.
Las instrucciones de instalación son los siguientes:
Selecciona la pestaña SOFTWARE y busca el DOWNLOAD para tu sistema operativo.
Una vez instalado Arduino IDE, entra a esta aplicación, selecciona el
menú
Archivo > Preferencias
.
Se abre un cuadro de diálogo y en el campo
Gestor de URLs Adicionales de Tarjetas
introduce el valor
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json,https://arduino.esp8266.com/stable/package_esp8266com_index.json
Selecciona el menú
Herramientas > Placa:xxx > Gestor de tarjetas…
.
En ocasiones el gestor de tarjetas tarda un poco en habilitarse, por
lo que tendrás que cerrar el menú y volver a abrirlo posiblemente unas
cuantas veces.
Instala las tarjetas esp32 y esp8266.
Selecciona el menú
Herramientas > Administrar bibliotecas…
.
En ocasiones el gestor de tarjetas tarda un poco en habilitarse, por
lo que tendrás que cerrar el menú y volver a abrirlo posiblemente unas
cuantas veces.
Instala la libresría EspMQTTClient. Se instala como una librería normal. Si te solicita instalar otras librerías, instálalas también.
En esta lección se introduce el concepto de PWA.
Aplicación que se entrega a través de la web, creada utilizando tecnologías web comunes como HTML, CSS y JavaScript.
Se conoce com PWA por sus siglas en inglés, que significan Progressive Web App.
Está destinado a funcionar en cualquier plataforma que use un navegador compatible con los estándares.
La funcionalidad incluye:
trabajar sin conexión,
notificaciones push y
acceso al hardware del dispositivo.
Esto permite crear experiencias de usuario similares a las aplicaciones nativas en dispositivos móviles y de escritorio.
Dado que una aplicación web progresiva es un tipo de aplicación web, no hay ningún requisito para que los desarrolladores o usuarios instalen las aplicaciones web a través de sistemas de distribución digital como Apple App Store o Google Play.
Es posible subirlas a Google Play y a Microsoft Store.
Fuente: https://es.wikipedia.org/wiki/Aplicaci%C3%B3n_web_progresiva
Un archivo de manifiesto web, con los campos correctos completados.
Un servidor web que utilice un dominio seguro (HTTPS).
Un conjunto de iconos para representar la aplicación en el dispositivo.
Un conjunto de capturas de pantallas con otrientaciones verticales y horizontales para mostrarlas al instalas la aplicación.
Un archivo service worker registrado para permitir que la aplicación funcione sin conexión.
Fuente: https://developer.mozilla.org/es/docs/Web/Progressive_web_apps/Installable_PWAs
Google Chrome.
Microsoft Edge (basado en Chromium).
Apple Safari (versión actualizada).
Firefox (no usa el archivo de manifiesto y no permite instalar las app).
Cualquier herramienta que permita editar HTML, CSS y JavaScript.
Un servidor web que use https.
Un editor de imágenes.
Generador de íconos enmascarables https://maskable.app/.
En esta lección se presentan los siguientes temas:
Definición de PWA.
Requerimientos de instalación y uso de una PWA.
Navegadores compatibles con PWA.
Herramientas de desarrollo.
En esta lección se presenta una PWA básica.
Puedes probar el ejemplo en https://gilpgpwa.github.io/.
También puedes probar el ejemplo en https://pwab.rf.gd/.
Prueba e instala, de preferencia con Chrome, el sitio https://pwab.rf.gd/ y en https://gilpgpwa.github.io/.
Descarga el archivo /src/pwab.zip y descompáctalo.
Crea tu proyecto en GitHub pages:
Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io
Mantén la selección Public.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Entra al repositorio y selecciona ⚙ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del
ejemplo, excepto el archivo .htaccess
.
Edita los archivos que desees.
Crea los íconos del proyecto con https://www.photopea.com/. Este paso se hizo com Microsoft Edge (versión Chromium), pero no funcionó con Google Chrome.
Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».
Coloca el archivo favicon.ico
en la raíz del proyecto.
Coloca los otros íconos en la carpeta img
y asegúrate de que estén
declarados en el archivo site.webmanifest
.
El archivo sw.js
tiene una lista de los archivos que se instalan.
El archivo instruccionesListadoSw.txt
te indica como generarla usando
Visual Studio Code.
Cada vez que modifiques los archivos, debes modificar el valor
de VERSION en el archivo sw.js
para poder ver los cambios
en el navegador.
Si tu proyecto no usa backend, haz clic derecho en
index.html
, selecciona Open with Live Server y se abre el
navegador para que puedas probar localmente el ejemplo.
Si tu proyecto usa PHP, haz clic derecho en
index.html
, selecciona PHP Server: serve project y se abre
el navegador para que puedas probar localmente el ejemplo.
Haz al menos una captura de la ventana de tu aplicación con orientación vertical y otra con orientación horizontal, todas ellas de 320 px como mínimo y 3,840 px como máximo.
Coloca las capturas en la carpeta img
y asegúrate de que estén
declaradas en el archivo site.webmanifest
.
Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en ves de ello desinstala la app:
Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).
En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.
Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página . Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.
Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.
En la Pestaña Aplicación (o Application en inglés) selecciona Archivo de manifiesto (o Manifest file en inglés). Esta herramienta analiza la estructura del archivo de manifiesto y te indica si hay un error. InfinityFree bloquea el análisis de las imágenes.
En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.
Cuando usas GitHub pages, antes de subir los archivos, no
debes modificar el valor de VERSION en el archivo sw.js
.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Si usas GitHub pages:
Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.
Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.
A partir de este momento, espera al menos 11 minutos para modificar el
valor de VERSION en el archivo sw.js
y volver a subir
el proyecto.
Si no usas GitHub pages:
Sube el proyecto al hosting que elijas sin incluir el archivo
.htaccess
. En algunos casos puedes usar
filezilla
(https://filezilla-project.org/)
En algunos host como InfinityFree, tienes que configurar el certificado SSL.
En algunos host, como InfinityFree, debes subir el
archivo .htaccess
cuando el certificado SSL se ha creado e
instalado. Sirve para forzar el uso de https, para eliminar el
control de cache, pues ahora lo lleva el service worker y para asignar el
mime type correcto para el archivo de manifest.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.
Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de instalar que aparece en el menú de extensión de Chrome.
Haz clic en los triángulos para expandir las carpetas
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | |
8 | <title>PWA Básica</title> |
9 | |
10 | <!-- Resumen para los motores de búsqueda. --> |
11 | <meta name="description" content="Ejemplo de PWA"> |
12 | |
13 | <script src="js/registraServiceWorker.js"></script> |
14 | |
15 | <meta name="viewport" content="width=device-width"> |
16 | |
17 | <!-- Color de la barra de navegación de Chrome en dispositivos móviles. --> |
18 | <meta name="theme-color" content="#cbc693"> |
19 | |
20 | <!-- Ícono para la página web, que normalmente se pone en la raíz del sitio. |
21 | Puede ser diferente para cada página. --> |
22 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
23 | |
24 | <!-- Configuración de la PWA para Chrome, Edge y Safari. |
25 | Debe ponerse en todas las páginas. --> |
26 | <link rel="manifest" href="site.webmanifest"> |
27 | |
28 | <link rel="stylesheet" href="css/estilos.css"> |
29 | |
30 | </head> |
31 | |
32 | <body> |
33 | |
34 | <h1>PWA Básica</h1> |
35 | |
36 | <p>Hola mundo.</p> |
37 | |
38 | </body> |
39 | |
40 | </html> |
1 | html { |
2 | color-scheme: light dark; |
3 | font-family: sans-serif; |
4 | } |
1 | "use strict" // usa JavaScript en modo estricto. |
2 | |
3 | const nombreDeServiceWorker = "sw.js" |
4 | |
5 | try { |
6 | navigator.serviceWorker.register(nombreDeServiceWorker) |
7 | .then(registro => { |
8 | console.log(nombreDeServiceWorker, "registrado.") |
9 | console.log(registro) |
10 | }) |
11 | .catch(error => console.log(error)) |
12 | } catch (error) { |
13 | console.log(error) |
14 | } |
Este archivo sirve para configurar los instaladores de la aplicación.
short_name
Nombre corto. Normalmente se despliega en dispositivos móviles. Máximo 20 caracteres.
name
Nombre largo. Normalmente se despliega en computadoras de escritorio. Máximo 30 caracteres.
id
Identificador del archivo de instalación. Normalmente es la ruta del archivo inicial de la app.
start_url
Ruta del archivo inicial de la app.
display
Forma de mostrar la app. El término standalone
significa que no
se muestra la barra de navegación del navegador web.
theme_color
Color de la barra de estado (en dispositivos móviles) o de título (en computadoras de escritorio) de la app.
background_color
Color de fondo de la pantalla desplah en dispositivos móviles.
description
Describe el propósito de la aplicación. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.
screenshots
Listado de máximo 8 capturas de pantalla. Aparecen en el cuadro de diálogo
que muestra el navegador al instalar la app. Debes incluir al menos una con
"form_factor": "wide"
y otra con
"form_factor": "narrow"
.
El ancho y la altura deben ser de 320 px como mínimo y 3,840 px como máximo.
La dimensión máxima no puede ser más de 2.3 veces mayor que la dimensión mínima.
Todas las capturas de pantalla con el valor del mismo factor de forma deben tener relaciones de aspecto idénticas.
Solo se admiten los formatos de imagen JPEG y PNG.
Solo se mostrarán ocho capturas de pantalla. Si se agregan más, el usuario-agente simplemente los ignora.
https://web.dev/patterns/web-apps/richer-install-ui?hl=es-419
icons
Listado de íconos en distintas resoluciones para los instaladores de la app. Se selecciona el que se vea mejor según las característocas del dispositivo.
src
Url de la imagen dentro de la app.
sizes
Dimensiones en pixeles de la imagen, anchoxalto.
type
Tipo mime de la imagen.
purpose
Forma en que se usa la imagen.
maskable
La imagen puede recortarse de forma segura para tomar distintas formas, como círculos, gotas, cuadrados con esquinas redondeadas, etc. Normalmente se usa para dispositivos móviles.
any
No se puede asegurar nada sobre la imagen. Normalmente se usa para dispositivos de escritorio.
Normalmente debe proporcionarse un juego de íconos con purpose
any
y otro juego de íconos con purpose
maskable
.
form_factor
Orientación de una screenshot.
wide
La screenshot tiene una orientación horizontal. Normalmente la creenshot se usa para dispositivos de escritorio.
narrow
La screenshot tiene una orientación vertical. Normalmente la creenshot se usa para dispositivos móviles.
Debes incluir al menos una screenshot con
"form_factor": "wide"
y otra con
"form_factor": "narrow"
.
label
Descripción de una screenshot. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.
1 | { |
2 | "short_name": "PWA", |
3 | "name": "Ejemplo de PWA", |
4 | "id": "/index.html", |
5 | "start_url": "/index.html", |
6 | "display": "standalone", |
7 | "theme_color": "#cbc693", |
8 | "background_color": "#ffffff", |
9 | "description": "Ejemplos básico de PWA.", |
10 | "screenshots": [ |
11 | { |
12 | "src": "/img/screenshot_horizontal.png", |
13 | "sizes": "1507x777", |
14 | "type": "image/png", |
15 | "form_factor": "wide", |
16 | "label": "PWA Básica" |
17 | }, |
18 | { |
19 | "src": "/img/screenshot_vertical.png", |
20 | "sizes": "591x980", |
21 | "type": "image/png", |
22 | "form_factor": "narrow", |
23 | "label": "PWA Básica (2)" |
24 | } |
25 | ], |
26 | "icons": [ |
27 | { |
28 | "src": "/img/maskable_icon_x48.png", |
29 | "sizes": "48x48", |
30 | "type": "image/png", |
31 | "purpose": "any" |
32 | }, |
33 | { |
34 | "src": "/img/maskable_icon_x72.png", |
35 | "sizes": "72x72", |
36 | "type": "image/png", |
37 | "purpose": "any" |
38 | }, |
39 | { |
40 | "src": "/img/maskable_icon_x96.png", |
41 | "sizes": "96x96", |
42 | "type": "image/png", |
43 | "purpose": "any" |
44 | }, |
45 | { |
46 | "src": "/img/maskable_icon_x128.png", |
47 | "sizes": "128x128", |
48 | "type": "image/png", |
49 | "purpose": "any" |
50 | }, |
51 | { |
52 | "src": "/img/maskable_icon_x192.png", |
53 | "sizes": "192x192", |
54 | "type": "image/png", |
55 | "purpose": "any" |
56 | }, |
57 | { |
58 | "src": "/img/maskable_icon_x384.png", |
59 | "sizes": "384x384", |
60 | "type": "image/png", |
61 | "purpose": "any" |
62 | }, |
63 | { |
64 | "src": "/img/maskable_icon_x512.png", |
65 | "sizes": "512x512", |
66 | "type": "image/png", |
67 | "purpose": "any" |
68 | }, |
69 | { |
70 | "src": "/img/maskable_icon.png", |
71 | "sizes": "2730x2730", |
72 | "type": "image/png", |
73 | "purpose": "any" |
74 | }, |
75 | { |
76 | "src": "/img/icono2048.png", |
77 | "sizes": "2048x2048", |
78 | "type": "image/png", |
79 | "purpose": "any" |
80 | }, |
81 | { |
82 | "src": "/img/maskable_icon_x48.png", |
83 | "sizes": "48x48", |
84 | "type": "image/png", |
85 | "purpose": "maskable" |
86 | }, |
87 | { |
88 | "src": "/img/maskable_icon_x72.png", |
89 | "sizes": "72x72", |
90 | "type": "image/png", |
91 | "purpose": "maskable" |
92 | }, |
93 | { |
94 | "src": "/img/maskable_icon_x96.png", |
95 | "sizes": "96x96", |
96 | "type": "image/png", |
97 | "purpose": "maskable" |
98 | }, |
99 | { |
100 | "src": "/img/maskable_icon_x128.png", |
101 | "sizes": "128x128", |
102 | "type": "image/png", |
103 | "purpose": "maskable" |
104 | }, |
105 | { |
106 | "src": "/img/maskable_icon_x192.png", |
107 | "sizes": "192x192", |
108 | "type": "image/png", |
109 | "purpose": "maskable" |
110 | }, |
111 | { |
112 | "src": "/img/maskable_icon_x384.png", |
113 | "sizes": "384x384", |
114 | "type": "image/png", |
115 | "purpose": "maskable" |
116 | }, |
117 | { |
118 | "src": "/img/maskable_icon_x512.png", |
119 | "sizes": "512x512", |
120 | "type": "image/png", |
121 | "purpose": "maskable" |
122 | }, |
123 | { |
124 | "src": "/img/maskable_icon.png", |
125 | "sizes": "2730x2730", |
126 | "type": "image/png", |
127 | "purpose": "maskable" |
128 | } |
129 | ] |
130 | } |
1 | Generar el listado de archivos del sw.js desde Visual Studio Code. |
2 | 1. Abrir una terminal desde el menú |
3 | Terminal > New Terminal |
4 | |
5 | 2. Desde la terminal introducir la orden: |
6 | Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt |
7 | |
8 | 3. Abrir el archivo generado, que se llama |
9 | archivos.txt |
10 | y sobre este, realizar los pasos que siguen: |
11 | |
12 | 4. Quita del archivo archivos.txt: |
13 | * el encabezado, |
14 | * todas las carpetas, |
15 | * el archivo .vscode/settings.json, |
16 | * el archivo .htaccess, |
17 | * el archivo archivos.txt, |
18 | * este archivo (instruccionesListadoSw.txt), |
19 | * el archivo jsconfig.json, |
20 | * el archivo sw.js, |
21 | * el archivo de la base de datos, que termina en ".bd" y |
22 | está en la carpeta srv, |
23 | * todos los archivos de php y |
24 | * las líneas en blanco del final |
25 | |
26 | 5. Cambia los \ por / desde Visual Studio Code con las siguientes |
27 | combinaciones de teclas: |
28 | |
29 | Ctrl+H En el diálogo que aparece introduce lo siguiente: |
30 | Find:\ |
31 | Replace:/ |
32 | |
33 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
34 | |
35 | 6. Coloca las comillas y coma del final de cada línea desde Visual |
36 | Studio Code con las siguientes combinaciones de teclas: |
37 | |
38 | Ctrl+H En el diálogo que aparece, selecciona el botón |
39 | ".*" |
40 | e introduce lo siguiente: |
41 | Find:\s*$ |
42 | Replace:", |
43 | |
44 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
45 | |
46 | 7. Marca la carpeta inicial, presiona la combinación de teclas: |
47 | |
48 | Shift+Ctrl+L |
49 | |
50 | borra la selección, teclea " y luego ESC |
51 | |
52 | 8. Cambia las secuencias de espacios por / con las siguientes |
53 | combinaciones de teclas: |
54 | |
55 | Ctrl+H En el diálogo que aparece, selecciona el botón |
56 | ".*" |
57 | e introduce lo siguiente: |
58 | Find:\s+ |
59 | Replace:/ |
60 | |
61 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
62 | |
63 | 9. Cambia las "/ por " con las siguientes combinaciones de teclas: |
64 | |
65 | Ctrl+H En el diálogo que aparece, quita la selección del botón |
66 | ".*" |
67 | e introduce lo siguiente: |
68 | Find:"/ |
69 | Replace:" |
70 | |
71 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
72 | |
73 | 10. Copia el texto al archivo |
74 | sw.js |
75 | en el contenido del arreglo llamado ARCHIVOS, pero recuerda |
76 | mantener el último elemento, que dice: |
77 | "/" |
1 | "favicon.ico", |
2 | "index.html", |
3 | "site.webmanifest", |
4 | "css/estilos.css", |
5 | "img/icono2048.png", |
6 | "img/maskable_icon.png", |
7 | "img/maskable_icon_x128.png", |
8 | "img/maskable_icon_x192.png", |
9 | "img/maskable_icon_x384.png", |
10 | "img/maskable_icon_x48.png", |
11 | "img/maskable_icon_x512.png", |
12 | "img/maskable_icon_x72.png", |
13 | "img/maskable_icon_x96.png", |
14 | "img/screenshot_horizontal.png", |
15 | "img/screenshot_vertical.png", |
16 | "js/registraServiceWorker.js", |
1 | /* Este archivo debe estar colocado en la carpeta raíz del sitio. |
2 | * |
3 | * Cualquier cambio en el contenido de este archivo hace que el service |
4 | * worker se reinstale. */ |
5 | |
6 | /** |
7 | * Cambia el número de la versión cuando cambia el contenido de los |
8 | * archivos. |
9 | * |
10 | * El número a la izquierda del punto (.), en este caso <q>1</q>, se |
11 | * conoce como número mayor y se cambia cuando se realizan |
12 | * modificaciones grandes o importantes. |
13 | * |
14 | * El número a la derecha del punto (.), en este caso <q>00</q>, se |
15 | * conoce como número menor y se cambia cuando se realizan |
16 | * modificaciones menores. |
17 | */ |
18 | const VERSION = "1.00" |
19 | |
20 | /** Nombre del archivo de cache. */ |
21 | const CACHE = "ejemploPWA" |
22 | |
23 | /** |
24 | * Archivos requeridos para que la aplicación funcione fuera de |
25 | * línea. |
26 | */ |
27 | const ARCHIVOS = [ |
28 | "favicon.ico", |
29 | "index.html", |
30 | "site.webmanifest", |
31 | "css/estilos.css", |
32 | "img/icono2048.png", |
33 | "img/maskable_icon.png", |
34 | "img/maskable_icon_x128.png", |
35 | "img/maskable_icon_x192.png", |
36 | "img/maskable_icon_x384.png", |
37 | "img/maskable_icon_x48.png", |
38 | "img/maskable_icon_x512.png", |
39 | "img/maskable_icon_x72.png", |
40 | "img/maskable_icon_x96.png", |
41 | "img/screenshot_horizontal.png", |
42 | "img/screenshot_vertical.png", |
43 | "js/registraServiceWorker.js", |
44 | "/" |
45 | ] |
46 | |
47 | // Verifica si el código corre dentro de un service worker. |
48 | if (self instanceof ServiceWorkerGlobalScope) { |
49 | // Evento al empezar a instalar el servide worker, |
50 | self.addEventListener("install", |
51 | (/** @type {ExtendableEvent} */ evt) => { |
52 | console.log("El service worker se está instalando.") |
53 | evt.waitUntil(llenaElCache()) |
54 | }) |
55 | |
56 | // Evento al solicitar información a la red. |
57 | self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => { |
58 | if (evt.request.method === "GET") { |
59 | evt.respondWith(buscaLaRespuestaEnElCache(evt)) |
60 | } |
61 | }) |
62 | |
63 | // Evento cuando el service worker se vuelve activo. |
64 | self.addEventListener("activate", |
65 | () => console.log("El service worker está activo.")) |
66 | } |
67 | |
68 | async function llenaElCache() { |
69 | console.log("Intentando cargar caché:", CACHE) |
70 | // Borra todos los cachés. |
71 | const keys = await caches.keys() |
72 | for (const key of keys) { |
73 | await caches.delete(key) |
74 | } |
75 | // Abre el caché de este service worker. |
76 | const cache = await caches.open(CACHE) |
77 | // Carga el listado de ARCHIVOS. |
78 | await cache.addAll(ARCHIVOS) |
79 | console.log("Cache cargado:", CACHE) |
80 | console.log("Versión:", VERSION) |
81 | } |
82 | |
83 | /** @param {FetchEvent} evt */ |
84 | async function buscaLaRespuestaEnElCache(evt) { |
85 | // Abre el caché. |
86 | const cache = await caches.open(CACHE) |
87 | const request = evt.request |
88 | /* Busca la respuesta a la solicitud en el contenido del caché, sin |
89 | * tomar en cuenta la parte después del símbolo "?" en la URL. */ |
90 | const response = await cache.match(request, { ignoreSearch: true }) |
91 | if (response === undefined) { |
92 | /* Si no la encuentra, empieza a descargar de la red y devuelve |
93 | * la promesa. */ |
94 | return fetch(request) |
95 | } else { |
96 | // Si la encuentra, devuelve la respuesta encontrada en el caché. |
97 | return response |
98 | } |
99 | } |
1 | AddType application/manifest+json .webmanifest |
2 | |
3 | ExpiresActive On |
4 | |
5 | Header set Cache-Control "max-age=1, must-revalidate" |
6 | |
7 | RewriteEngine On |
8 | |
9 | RewriteCond %{HTTP:X-Forwarded-Proto} !https |
10 | RewriteCond %{HTTPS} off |
11 | RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"} |
12 | RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] |
13 |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
1 | { |
2 | "compilerOptions": { |
3 | "checkJs": true, |
4 | "strictNullChecks": true, |
5 | "target": "ES6", |
6 | "module": "Node16", |
7 | "moduleResolution": "Node16", |
8 | "lib": [ |
9 | "ES2017", |
10 | "WebWorker", |
11 | "DOM" |
12 | ] |
13 | } |
14 | } |
En esta lección se presentó la estructura básica de una PWA.
En esta lección se presenta una PWA con Material Design, explica como construir vistas y usar componentes.
Puedes probar el ejemplo en https://gilpgpwamd.github.io/index.html.
También puedes probar el ejemplo en https://pwamd.rf.gd/.
Material Design es una guía de diseño para aplicaciones multiplataforma. La encuentras en https://m3.material.io/.
La forma de adaptar Material Design en distintas plataformas está en https://material.io/design/platform-guidance/cross-platform-adaptation.html
Prueba e instala, de preferencia con Chrome, el sitio https://pwamd.rf.gd/ y en https://gilpgpwamd.github.io/.
Descarga el archivo /src/pwamd.zip y descompáctalo.
Crea tu proyecto en GitHub pages:
Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io
Mantén la selección Public.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Entra al repositorio y selecciona ⚙ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del
ejemplo, excepto el archivo .htaccess
.
Edita los archivos que desees.
Crea los íconos del proyecto con https://www.photopea.com/. Este paso se hizo com Microsoft Edge (versión Chromium), pero no funcionó con Google Chrome.
Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».
Coloca el archivo favicon.ico
en la raíz del proyecto.
Coloca los otros íconos en la carpeta img
y asegúrate de que estén
declarados en el archivo site.webmanifest
.
Para cambiar los colores, entra al sitio
https://material-foundation.github.io/material-theme-builder/,
selecciona los colores de tu aplicación, haz clic en
Pick your fonts →. Elige font Roboto, haz clic en
Export theme →. Haz clic en
Export y selecciona Web (CSS) para descargar el zip que
contiene los estilos que generan los colores. Descompacta el zip y copia los
archivos de la carpeta css
a la carpeta css
del proyecto,
sobreescribiendo los archivos del ejemplo.
El archivo sw.js
tiene una lista de los archivos que se instalan.
El archivo instruccionesListadoSw.txt
te indica como generarla usando
Visual Studio Code.
Cada vez que modifiques los archivos, debes modificar el valor
de VERSION en el archivo sw.js
para poder ver los cambios
en el navegador.
Si tu proyecto no usa backend, haz clic derecho en
index.html
, selecciona Open with Live Server y se abre el
navegador para que puedas probar localmente el ejemplo.
Si tu proyecto usa PHP, haz clic derecho en
index.html
, selecciona PHP Server: serve project y se abre
el navegador para que puedas probar localmente el ejemplo.
Haz al menos una captura de la ventana de tu aplicación con orientación vertical y otra con orientación horizontal, todas ellas de 320 px como mínimo y 3,840 px como máximo.
Coloca las capturas en la carpeta img
y asegúrate de que estén
declaradas en el archivo site.webmanifest
.
Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en ves de ello desinstala la app:
Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).
En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.
Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página . Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.
Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.
En la Pestaña Aplicación (o Application en inglés) selecciona Archivo de manifiesto (o Manifest file en inglés). Esta herramienta analiza la estructura del archivo de manifiesto y te indica si hay un error. InfinityFree bloquea el análisis de las imágenes.
En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.
Para depurar paso a paso haz lo siguiente:
En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.
Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).
Selecciona el archivo donde vas a empezar a depurar.
Haz clic en el número de la línea donde vas a empezar a depurar.
Recarga la página de manera normal.
Empieza a usar tu sitio.
Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.
Cuando usas GitHub pages, antes de subir los archivos, no
debes modificar el valor de VERSION en el archivo sw.js
.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Si usas GitHub pages:
Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.
Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.
A partir de este momento, espera al menos 11 minutos para modificar el
valor de VERSION en el archivo sw.js
y volver a subir
el proyecto.
Si no usas GitHub pages:
Sube el proyecto al hosting que elijas sin incluir el archivo
.htaccess
. En algunos casos puedes usar
filezilla
(https://filezilla-project.org/)
En algunos host como InfinityFree, tienes que configurar el certificado SSL.
En algunos host, como InfinityFree, debes subir el
archivo .htaccess
cuando el certificado SSL se ha creado e
instalado. Sirve para forzar el uso de https, para eliminar el
control de cache, pues ahora lo lleva el service worker y para asignar el
mime type correcto para el archivo de manifest.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Instala y usa tu PWA en Windows. Aunque en este video se recomienda usar Edge, al momento de actualizar el contenido, la opción más recomendada es Chrome para que te muestre las descripciones y las capturas de pantalla.
Instala y usa tu PWA en Android. Al momento de actualizar las notas, tal vez no te aparezca el botón para instalar y tengas que seleccionar la acción de instalar que aparece en el menú de extensión de Chrome.
Haz clic en los triángulos para expandir las carpetas
La página principal de las aplicaciónes móviles incluye una barra de aplicación centrada, cuyo nombre oficial es centered aligned top app bar, que contiene:
El título de la aplicación, que se muestra centrado,
al inicio, de manera opcional, un botón de ícono navegación y
al final, un botón de acción, mostrando un ícono, que puede servir para acceder al perfil del usuario, mostrando su avatar, acceder a la configuración de la app, mostrando un engrane, o alguna otra acción.
Para que el contenido se muestra adecuadamente, se usa el elemento
personalizado
md-top-app-bar,
cuyo comportamiento está definido en la clase
MdTopAppBar.
Hay que añadirle class="center-aligned"
Para el título, dentro de la la barra de aplicación se usa un elemento de
tipo
h1
.
El botón opcional de navegación debe llevar al atributo
slot="navigation"
.
El botón opcional de acción debe llevar al atributo
slot="action"
.
En los ejemplos con pestañas se muestra su uso.
El botón de perfil debe llevar la clase
avatar
.
Para añadir elementos debajo del título, indica su id con el atributo
adicional
. Los ejemplos sobre
pestañas indican como.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>PWA con MD</title> |
8 | |
9 | <!-- Resumen para los motores de búsqueda. --> |
10 | <meta name="description" content="Ejemplo de PWA con Material Design"> |
11 | |
12 | <script src="js/registraServiceWorker.js"></script> |
13 | |
14 | <meta name="viewport" content="width=device-width"> |
15 | |
16 | <!-- Color de la barra de navegación de Chrome en dispositivos móviles. --> |
17 | <meta name="theme-color" content="#fffbfe"> |
18 | |
19 | <!-- Ícono para la página web, que normalmente se pone en la raíz del sitio. |
20 | Puede ser diferente para cada página. --> |
21 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
22 | |
23 | <!-- Configuración de la PWA para Chrome, Edge y Safari. |
24 | Debe ponerse en todas las páginas. --> |
25 | <link rel="manifest" href="site.webmanifest"> |
26 | |
27 | <!-- Permite a los navegadores, que como Safari no soportan el estándar |
28 | completo de custom elementsm, lo cumplan totalmente. Debe ponerse en todas |
29 | las páginas. --> |
30 | <script src="ungap/custom-elements.js"></script> |
31 | |
32 | <!-- Configuración de todas las páginas con JavaSript. --> |
33 | <script type="module" src="js/configura.js"></script> |
34 | <link rel="stylesheet" href="css/estilos.css"> |
35 | <link rel="stylesheet" href="css/transicion_completa.css"> |
36 | |
37 | </head> |
38 | |
39 | <body> |
40 | |
41 | <md-top-app-bar class="center-aligned"> |
42 | |
43 | <h1>PWA con MD</h1> |
44 | |
45 | <button is="md-menu-button" slot="navigation"></button> |
46 | |
47 | <button type="button" class="md-standard-icon-button avatar" title="Perfil" |
48 | slot="action"> |
49 | <span class="material-symbols-outlined">account_circle</span> |
50 | </button> |
51 | |
52 | </md-top-app-bar> |
53 | |
54 | <main> |
55 | <p> |
56 | Esta es la página principal de la app. Las X que siguen son para que veas |
57 | como se comporta cuando se hace scroll. |
58 | </p> |
59 | <p>x</p> |
60 | <p>x</p> |
61 | <p>x</p> |
62 | <p>x</p> |
63 | <p>x</p> |
64 | <p>x</p> |
65 | <p>x</p> |
66 | <p>x</p> |
67 | <p>x</p> |
68 | <p>x</p> |
69 | <p>x</p> |
70 | <p>x</p> |
71 | <p>x</p> |
72 | <p>x</p> |
73 | <p>x</p> |
74 | <p>x</p> |
75 | <p>x</p> |
76 | <p>x</p> |
77 | <p>x</p> |
78 | <p>x</p> |
79 | <p>x</p> |
80 | <p>x</p> |
81 | <p>x</p> |
82 | </main> |
83 | |
84 | <nav-drw></nav-drw> |
85 | |
86 | </body> |
87 | |
88 | </html> |
Las barras de aplicación en Material Design están definidas en https://m3.material.io/components/top-app-bar/overview.
Cuando la barra de aplicación no está en la página principal, puede mostrar hasta 3 botones de ícono a la derecha. Deben ser las acciones más usadas.
Cuando hay más de 3 acciones, coloca las menos usadas en el overflow menu, que se despliega cliqueando el botón con el ícono ⋯ en iOS, y el ícono ⁝ en otros sistemas operativos.
Cuando en iOS no hay íconos de acción, el título debe ir centrado.
Para que el contenido se muestra adecuadamente, se usa el elemento personalizado md-top-app-bar, cuyo comportamiento está definido en la clase MdTopAppBar.
El boton de overflow debe ser el último con slot="action"
. Usa
el atributo is="md-overflow-button". Su atributo onclick
debe
alternar el overflow menu.
El overflow menu, se define con el elemento personalizado mdoverflow-menu, cuyo comportamiento está definido en la clase MdOverflowMenu.
Para el título, dentro de la la barra de aplicación se usa un elemento de
tipo
h1
.
El texto debe ser corto y caber en un solo renglón, o de lo conrario se
cortará.
Para mostrar un título más largo, hay que poner después de la barra de
aplicación otro h1
y ponerle un
id
. Ese
id
se pone como valor del atributo
headline
dentro de la md-top-app-bar
.
Cuando la página tenga el scroll hasta arriba, se despliega el
h1
que está dentro del
md-top-app-bar
..
Al hacer scroll, este título se compacta y se muestra el título definido con
el atributo headline
.
Si deseas que la letra del encabezado largo sea más pequeña, usa la
clase medium
dentro de la md-top-app-bar
.
El botón de navegación opcional debe llevar el atributo
slot="navigation"
.
Los botones de acción deben llevar el atributo
slot="action"
.
El overflow menú debe llevar el atributo
slot="overflow"
.
Para añadir elementos debajo del título, usa el atributo
slot="adicional"
.
El botón de ícono para abrir el overflow menu, se coloca dentro de este menú
usando el atributo
slot="icon"
.
Los botones de acción se colocan dentro del overflow menu sin ningún
atributo
slot
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Vista secundaria - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar class="medium" headline="h1Headline"> |
26 | |
27 | <h1>Secundaria</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | <button type="button" class="md-standard-icon-button" title="Agregar" |
32 | slot="action"> |
33 | <span class="material-symbols-outlined">add</span> |
34 | </button> |
35 | |
36 | <button type="button" class="md-standard-icon-button" title="Editar" |
37 | slot="action"> |
38 | <span class="material-symbols-outlined">edit</span> |
39 | </button> |
40 | |
41 | <button is="md-overflow-button" slot="action" |
42 | onclick="overflow.alterna(this)"></button> |
43 | |
44 | </md-top-app-bar> |
45 | |
46 | <h1 id="h1Headline">Página secundaria</h1> |
47 | |
48 | <main> |
49 | <p> |
50 | Esta es una página secundaria de la app. Las X que siguen son para que veas |
51 | como se comporta cuando se hace scroll. |
52 | </p> |
53 | <p>x</p> |
54 | <p>x</p> |
55 | <p>x</p> |
56 | <p>x</p> |
57 | <p>x</p> |
58 | <p>x</p> |
59 | <p>x</p> |
60 | <p>x</p> |
61 | <p>x</p> |
62 | <p>x</p> |
63 | <p>x</p> |
64 | <p>x</p> |
65 | <p>x</p> |
66 | <p>x</p> |
67 | <p>x</p> |
68 | <p>x</p> |
69 | <p>x</p> |
70 | <p>x</p> |
71 | <p>x</p> |
72 | <p>x</p> |
73 | <p>x</p> |
74 | <p>x</p> |
75 | <p>x</p> |
76 | </main> |
77 | |
78 | <md-overflow-menu id="overflow"> |
79 | |
80 | <button type="button"> |
81 | <span class="material-symbols-outlined">star</span> |
82 | Marcar favorito |
83 | </button> |
84 | |
85 | <button type="button"> |
86 | <span class="material-symbols-outlined"> delete</span> |
87 | Eliminar |
88 | </button> |
89 | |
90 | </md-overflow-menu> |
91 | |
92 | <nav-drw></nav-drw> |
93 | |
94 | </body> |
95 | |
96 | </html> |
Las barras de aplicación en Material Design están definidas en https://m3.material.io/components/top-app-bar/overview.
Los menús en Material Design están definidos en https://m3.material.io/components/menus/overview.
El sitio oficial de los íconos de Material Design es https://fonts.google.com/icons.
En el sitio oficial te aparecen los íconos en diferentes estilos.
Si seleccionas un ícono y su estilo, te aparecen las instrucciones de como añadirlo a tu código.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Íconos - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar> |
26 | |
27 | <h1>Íconos</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <main> |
34 | |
35 | <button type="button" class="md-standard-icon-button"> |
36 | <span class="material-symbols-outlined">favorite</span> |
37 | </button> |
38 | |
39 | <button type="button" class="md-standard-icon-button" disabled> |
40 | <span class="material-symbols-outlined">bolt</span> |
41 | </button> |
42 | |
43 | <a class="md-standard-icon-button" target="_blank" rel="noreferrer" |
44 | href="https://google.com"> |
45 | <span class="material-symbols-outlined">star</span></a> |
46 | |
47 | <span class="material-symbols-outlined">thumb_up</span> |
48 | |
49 | <button type="button" class="md-fab-primary" |
50 | style="position: fixed; bottom: 1rem; right: 1rem;"> |
51 | <span class="material-symbols-outlined">add</span> |
52 | </button> |
53 | |
54 | </main> |
55 | |
56 | <nav-drw></nav-drw> |
57 | |
58 | </body> |
59 | |
60 | </html> |
Los botones en Material Design están definidos en https://m3.material.io/components/icon-buttons/overview.
Los botones FAB en Material Design están definidos en https://m3.material.io/components/floating-action-button/overview.
Los botones principales deben usar la clase md-filled-button. Solo se pone un botón principal por pagina.
Los botones secundarios deben usar la clase md-outline-button.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Botones - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar> |
26 | |
27 | <h1>Botones</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <main> |
34 | |
35 | <p> |
36 | |
37 | <button class="md-filled-button"> |
38 | Primario |
39 | </button> |
40 | |
41 | <button class="md-outline-button"> |
42 | Secundario |
43 | </button> |
44 | |
45 | </p> |
46 | |
47 | </main> |
48 | |
49 | <nav-drw></nav-drw> |
50 | |
51 | </body> |
52 | |
53 | </html> |
Los botones en Material Design están definidos en https://m3.material.io/components/buttons/overview.
Se usa una etiqueta flotante que se muestra grande cuando el contenido de campo es una cadena vacía y se muestra pequeña cuando el elemento se está capturando o cuando su contenido no está vacío.
Añade la class
md-filled-text-field
a un elemento de tipo
label
,
span
,
p
o
div
Usa la class
float
junto a la class
md-filled-text-field
cuando quieras que la etiqueta esté arriba todo el tiempo. Esto sirve para
los
output
,
input type="date"
e
input type="file"
entre otros tipos de elemento.
El rótulo de las etiquetas deben ser
span
o
label
y deben colocarse inmediatamente después del elemento que realiza la
captura.
Para que la etiquta flote correctamente, el elemento de captura debe
repetir el rótulo de la etiqueta en el atributo
placeholder
.
Los textos de ayuda deben ser elementos de tipo
small
que se colocan después de la etiqueta.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Campos de texto - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar headline="headline"> |
26 | |
27 | <h1>Texto</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <h1 id="headline">Campos de texto</h1> |
34 | |
35 | <main> |
36 | |
37 | <!-- Usa |
38 | class="float" |
39 | cuando quieras que la etiqueta esté arriba todo el tiempo. --> |
40 | <p> |
41 | <label class="md-filled-text-field float"> |
42 | <output>Saludo</output> |
43 | <span>Saludo</span> |
44 | </label> |
45 | </p> |
46 | |
47 | <p> |
48 | <label class="md-filled-text-field"> |
49 | <input required placeholder="Nombre*"> |
50 | <span>Nombre *</span> |
51 | <small>Obligatorio</small> |
52 | </label> |
53 | </p> |
54 | |
55 | <p> |
56 | <label class="md-filled-text-field"> |
57 | <input type="email" placeholder="Email"> |
58 | <span accesskey="M">Email</span> |
59 | </label> |
60 | </p> |
61 | |
62 | <p> |
63 | <label class="md-filled-text-field float"> |
64 | <input type="date" placeholder="Fecha"> |
65 | <span>Fecha</span> |
66 | </label> |
67 | </p> |
68 | |
69 | <p> |
70 | <label class="md-filled-text-field"> |
71 | <textarea rows="3" placeholder="Dirección"></textarea> |
72 | <span>Dirección</span> |
73 | </label> |
74 | </p> |
75 | |
76 | </main> |
77 | |
78 | <nav-drw></nav-drw> |
79 | |
80 | </body> |
81 | |
82 | </html> |
Los campos de texto en Material Design están definidos en https://m3.material.io/components/text-fields/overview.
Los menús en Material Design están definidos en https://material.io/components/menus.
Este control se adapta mejor a Material Designe que el select de HTML.
Usa el elemento personalizado md-select-menu, definido con la clase MdSelectMenu.
Coloca el elemento md-select-menu
con
slot="input-text"
,
dentro de un
<p class="md-filled-text-field">
.
Usa un span
cono etiqueta.
Para asignar un valor inicial, usa el atributo
value
al
md-select-menu
.
Para que forzosamente se deba seleccionar una opción, agrega el atributo
booleano
required
al
md-select-menu
.
Para las opciones usa el elemento personalizado
md-options-menu, definido con la clase
MdOptionMenu.
Dentro de él coloca elementos tipo
span
que indican el value de la opción con el atributo
data-value
y cuyo contenido es el texto
de la opción.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Select - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <form> |
26 | |
27 | <md-top-app-bar> |
28 | |
29 | <h1>Select</h1> |
30 | |
31 | <button is="md-menu-button" slot="navigation"></button> |
32 | |
33 | </md-top-app-bar> |
34 | |
35 | <main> |
36 | |
37 | <p> |
38 | <span id="etiquetaGrupo" class="md-filled-text-field" accesskey="G"> |
39 | <md-select-menu name="grupo" required value="IC21" |
40 | aria-labelledby="etiquetaGrupo" |
41 | options="opcionesDeGrupo"></md-select-menu> |
42 | <span>Grupo *</span> |
43 | <small>Obligatorio</small> |
44 | </span> |
45 | </p> |
46 | |
47 | <p> |
48 | <button class="md-filled-button" style="width: 100%;">Enviar</button> |
49 | </p> |
50 | |
51 | </main> |
52 | |
53 | <md-options-menu id="opcionesDeGrupo" aria-label="Opciones de grupo"> |
54 | <span data-value="" title="Selecciona opción"></span> |
55 | <span data-value="IC21">IC-21</span> |
56 | <span data-value="IC22">IC-22</span> |
57 | <span data-value="IC23">IC-23</span> |
58 | </md-options-menu> |
59 | |
60 | <nav-drw></nav-drw> |
61 | |
62 | </form> |
63 | |
64 | </body> |
65 | |
66 | </html> |
Los campos de texto en Material Design están definidos en https://m3.material.io/components/text-fields/overview.
Los menús en Material Design están definidos en https://m3.material.io/components/menus/overview.
Usa un elemento
input
con el atribto
type="checkbox"
y
class="md-switch"
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Interruptores - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar> |
26 | |
27 | <h1>Interruptores</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <main> |
34 | <p> |
35 | <label accesskey="R"> |
36 | Muestra resultados |
37 | <input type="checkbox" class="md-switch"> |
38 | </label> |
39 | </p> |
40 | </main> |
41 | |
42 | <nav-drw></nav-drw> |
43 | |
44 | </body> |
45 | |
46 | </html> |
Los interruptores en Material Design están defnidos en https://m3.material.io/components/switch/overview.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Sliders - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar> |
26 | |
27 | <h1>Sliders</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <main> |
34 | |
35 | <md-slider-field> |
36 | <label>Rango</label> |
37 | <input type="range" slot="slider" min="1" max="10"> |
38 | <small slot="supporting">Calcúlalo bien</small> |
39 | </md-slider-field> |
40 | |
41 | </main> |
42 | |
43 | <nav-drw></nav-drw> |
44 | |
45 | </body> |
46 | |
47 | </html> |
Los sliders en Material Design están definidos en https://m3.material.io/components/sliders/overview.
Permiten selecionar de entre 2 a 5 opciones.
La base es un elemento con
class="md-segmented-button"
.
Puedes usar
type="radio"
para seleccionar una sola opción para un mismo valor del atributo
name
.
Puedes usar
type="checkbox"
para seleccionar varias opciones con un mismo valor del atributo
name
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Botón segmentado - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar headline="headline"> |
26 | |
27 | <h1>Segmentado</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <h1 id="headline">Botón segmentado</h1> |
34 | |
35 | <main> |
36 | |
37 | <p class="md-segmented-button"> |
38 | |
39 | <input id="bueno" type="radio" name="estado" checked value="2"> |
40 | <label for="bueno"> |
41 | <span class="material-symbols-outlined">done</span> |
42 | Bueno |
43 | </label> |
44 | |
45 | <input id="intermedio" type="radio" name="estado" value="1"> |
46 | <label for="intermedio"> |
47 | <span class="material-symbols-outlined">done</span> |
48 | Intermedio |
49 | </label> |
50 | |
51 | <input id="malo" type="radio" name="estado" value="0"> |
52 | <label for="malo"> |
53 | <span class="material-symbols-outlined">done</span> |
54 | Malo |
55 | </label> |
56 | |
57 | </p> |
58 | |
59 | </main> |
60 | |
61 | <nav-drw></nav-drw> |
62 | |
63 | </body> |
64 | |
65 | </html> |
Los botones segmentados de Material Design están defnidos en https://m3.material.io/components/segmented-buttons/overview.
Las listas de Material Design usan un
ul
con la clase
md-list
.
Las listas pueden usar
li
con las clases
one-line
,
two-line
o
three-line
Si el elemento es un hipervínculo, el elemento
li
no lleva clases y en su interior se pone una
a
con la clase
one-line
,
two-line
o
three-line
Los elementos de la clase
one-line
,
tienen el siguiente contenido:
Un encabezado con un elemento
span class="headline"
que muestra texto resaltado truncado a un renglón.
Opcionalmente muestran una imagen a la izquierda, que puede ser:
Se añade la clase
icon
junto a la clase
one-line
.
Se añade un
img
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
one-line
.
Se añade la clase
image
junto a la clase
one-line
.
Se añade un
img
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
one-line
.
Se añade la clase
video
junto a la clase
one-line
.
Se añade un
img
dentro del elemento con la clase
one-line
.
Se añade la clase
avatar
junto a la clase
one-line
.
Se añade un
img
,
un
label
con las iniciales del usuario
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
one-line
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Listas one-line - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar headline="headline"> |
26 | |
27 | <h1>one-line</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <h1 id="headline">Listas one-line</h1> |
34 | |
35 | <main> |
36 | |
37 | <ul class="md-list"> |
38 | |
39 | <li class="md-one-line"> |
40 | <span class="headline"> |
41 | Ciudad de México |
42 | </span> |
43 | </li> |
44 | |
45 | <li class="md-one-line"> |
46 | <span class="headline"> |
47 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
48 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
49 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
50 | alias! |
51 | </span> |
52 | </li> |
53 | |
54 | <li class="md-one-line icon"> |
55 | <span class="material-symbols-outlined">account_balance</span> |
56 | <span class="headline"> |
57 | Atenas |
58 | </span> |
59 | </li> |
60 | |
61 | <li class="md-one-line avatar"> |
62 | <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg"> |
63 | <span class="headline"> |
64 | Ana |
65 | </span> |
66 | </li> |
67 | |
68 | <li class="md-one-line image"> |
69 | <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg"> |
70 | <span class="headline"> |
71 | Neza |
72 | </span> |
73 | </li> |
74 | |
75 | <li class="md-one-line video"> |
76 | <img alt="Ciudad de San Francisco" |
77 | src="img/pexels-craig-dennis-3701822.jpg"> |
78 | <span class="headline"> |
79 | San Francisco |
80 | </span> |
81 | </li> |
82 | |
83 | <li class="md-one-line video"> |
84 | <img alt="Ciudad de San Francisco" |
85 | src="img/pexels-craig-dennis-3701822.jpg"> |
86 | <span class="headline"> |
87 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
88 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
89 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
90 | </span> |
91 | </li> |
92 | |
93 | <li> |
94 | <a class="md-one-line image" target="_blank" rel=”noreferrer” |
95 | href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/"> |
96 | <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg"> |
97 | <span class="headline"> |
98 | Neza Link |
99 | </span> |
100 | </a> |
101 | </li> |
102 | |
103 | </ul> |
104 | |
105 | <footer> |
106 | <ul> |
107 | <li> |
108 | <p> |
109 | <small> |
110 | <a target="_blank" rel=”noreferrer” |
111 | href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/"> |
112 | La foto de la niña es de Moises Patrício, publicada en el sitio Pexels. |
113 | Haz clic en este hipervínculo para más información. |
114 | </a> |
115 | </small> |
116 | </p> |
117 | </li> |
118 | <li> |
119 | <p> |
120 | <small> |
121 | <a target="_blank" rel=”noreferrer” |
122 | href="https://www.pinterest.com.mx/ludresi/"> |
123 | La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio |
124 | Pinterest. Haz clic en este hipervínculo para más información. |
125 | </a> |
126 | </small> |
127 | </p> |
128 | </li> |
129 | <li> |
130 | <p> |
131 | <small> |
132 | <a target="_blank" rel=”noreferrer” |
133 | href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/"> |
134 | La foto del puente de San Francisco es de Craig Dennis, publicada en el |
135 | sitio Pexels. Haz clic en este hipervínculo para más información. |
136 | </a> |
137 | </small> |
138 | </p> |
139 | </li> |
140 | </ul> |
141 | </footer> |
142 | |
143 | </main> |
144 | |
145 | <nav-drw></nav-drw> |
146 | |
147 | </body> |
148 | |
149 | </html> |
Las listas están definida en https://m3.material.io/components/lists/overview.
Las listas de Material Design usan un
ul
con la clase
md-list
.
Las listas pueden usar
li
con las clases
one-line
,
two-line
o
three-line
Si el elemento es un hipervínculo, el elemento
li
no lleva clases y en su interior se pone una
a
con la clase
one-line
,
two-line
o
three-line
Los elementos de la clase
two-line
,
tienen el siguiente contenido:
Un encabezado con un elemento
span class="headline"
que muestra texto resaltado truncado a un renglón.
Un elemento
span class="supporting"
que muestra texto mas pequeño y truncado a un renglón.
Opcionalmente muestran una imagen a la izquierda, que puede ser:
Se añade la clase
icon
junto a la clase
two-line
.
Se añade un
img
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
two-line
.
Se añade la clase
image
junto a la clase
two-line
.
Se añade un
img
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
two-line
.
Se añade la clase
video
junto a la clase
two-line
.
Se añade un
img
dentro del elemento con la clase
two-line
.
Se añade la clase
avatar
junto a la clase
two-line
.
Se añade un
img
,
un
label
con las iniciales del usuario
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
two-line
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Listas two-line - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar headline="headline"> |
26 | |
27 | <h1>two-line</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <h1 id="headline">Listas two-line</h1> |
34 | |
35 | <main> |
36 | |
37 | <ul class="md-list"> |
38 | |
39 | <li class="md-two-line"> |
40 | <span class="headline"> |
41 | Ciudad de México 2 |
42 | </span> |
43 | <span class="supporting"> |
44 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
45 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
46 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
47 | alias! |
48 | </span> |
49 | </li> |
50 | |
51 | <li class="md-two-line icon"> |
52 | <span class="material-symbols-outlined">account_balance</span> |
53 | <span class="headline"> |
54 | Atenas 2 |
55 | </span> |
56 | <span class="supporting"> |
57 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
58 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
59 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
60 | alias! |
61 | </span> |
62 | </li> |
63 | |
64 | <li class="md-two-line avatar"> |
65 | <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg"> |
66 | <span class="headline"> |
67 | Ana 2 |
68 | </span> |
69 | <span class="supporting"> |
70 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
71 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
72 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
73 | alias! |
74 | </span> |
75 | </li> |
76 | |
77 | <li class="md-two-line image"> |
78 | <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg"> |
79 | <span class="headline"> |
80 | Neza 2 |
81 | </span> |
82 | <span class="supporting"> |
83 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
84 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
85 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
86 | alias! |
87 | </span> |
88 | </li> |
89 | |
90 | <li class="md-two-line video"> |
91 | <img alt="Ciudad de San Francisco" |
92 | src="img/pexels-craig-dennis-3701822.jpg"> |
93 | <span class="headline"> |
94 | San Francisco 2 |
95 | </span> |
96 | <span class="supporting"> |
97 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
98 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
99 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
100 | alias! |
101 | </span> |
102 | </li> |
103 | |
104 | <li> |
105 | <a class="md-two-line image" target="_blank" rel="noreferrer" |
106 | href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/"> |
107 | <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg"> |
108 | <span class="headline"> |
109 | Neza Link 2 |
110 | </span> |
111 | <span class="supporting"> |
112 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
113 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
114 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
115 | alias! |
116 | </span> |
117 | </a> |
118 | </li> |
119 | |
120 | </ul> |
121 | |
122 | <footer> |
123 | <ul> |
124 | <li> |
125 | <p> |
126 | <small> |
127 | <a target="_blank" rel="noreferrer" |
128 | href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/"> |
129 | La foto de la niña es de Moises Patrício, publicada en el sitio Pexels. |
130 | Haz clic en este hipervínculo para más información. |
131 | </a> |
132 | </small> |
133 | </p> |
134 | </li> |
135 | <li> |
136 | <p> |
137 | <small> |
138 | <a target="_blank" rel=”noreferrer” |
139 | href="https://www.pinterest.com.mx/ludresi/"> |
140 | La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio |
141 | Pinterest. Haz clic en este hipervínculo para más información. |
142 | </a> |
143 | </small> |
144 | </p> |
145 | </li> |
146 | <li> |
147 | <p> |
148 | <small> |
149 | <a target="_blank" rel=”noreferrer” |
150 | href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/"> |
151 | La foto del puente de San Francisco es de Craig Dennis, publicada en el |
152 | sitio Pexels. Haz clic en este hipervínculo para más información. |
153 | </a> |
154 | </small> |
155 | </p> |
156 | </li> |
157 | </ul> |
158 | </footer> |
159 | |
160 | </main> |
161 | |
162 | <nav-drw></nav-drw> |
163 | |
164 | </body> |
165 | |
166 | </html> |
Las listas están definida en https://m3.material.io/components/lists/overview.
Las listas de Material Design usan un
ul
con la clase
md-list
.
Las listas pueden usar
li
con las clases
one-line
,
two-line
o
three-line
Si el elemento es un hipervínculo, el elemento
li
no lleva clases y en su interior se pone una
a
con la clase
one-line
,
two-line
o
three-line
Los elementos de la clase
three-line
,
tienen el siguiente contenido:
Un encabezado con un elemento
span class="headline"
que muestra texto resaltado truncado a un renglón.
Un elemento
span class="supporting"
que muestra texto mas pequeño y truncado 2 renglones.
Opcionalmente muestran una imagen a la izquierda, que puede ser:
Se añade la clase
icon
junto a la clase
three-line
.
Se añade un
img
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
three-line
.
Se añade la clase
image
junto a la clase
three-line
.
Se añade un
img
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
three-line
.
Se añade la clase
video
junto a la clase
three-line
.
Se añade un
img
dentro del elemento con la clase
three-line
.
Se añade la clase
avatar
junto a la clase
three-line
.
Se añade un
img
,
un
label
con las iniciales del usuario
o un
span class="material-symbols-outlined"
dentro del elemento con la clase
three-line
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Listas tree-line - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar headline="headline"> |
26 | |
27 | <h1>tree-line</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <h1 id="headline">Listas tree-line</h1> |
34 | |
35 | <main> |
36 | |
37 | <ul class="md-list"> |
38 | |
39 | <li class="md-three-line"> |
40 | <span class="headline"> |
41 | Ciudad de México 3 |
42 | </span> |
43 | <span class="supporting"> |
44 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
45 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
46 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
47 | alias! |
48 | </span> |
49 | </li> |
50 | |
51 | <li class="md-three-line icon"> |
52 | <span class="material-symbols-outlined">account_balance</span> |
53 | <span class="headline"> |
54 | Atenas 3 |
55 | </span> |
56 | <span class="supporting"> |
57 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
58 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
59 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
60 | alias! |
61 | </span> |
62 | </li> |
63 | |
64 | <li class="md-three-line avatar"> |
65 | <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg"> |
66 | <span class="headline"> |
67 | Ana 3 |
68 | </span> |
69 | <span class="supporting"> |
70 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
71 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
72 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
73 | alias! |
74 | </span> |
75 | </li> |
76 | |
77 | <li class="md-three-line image"> |
78 | <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg"> |
79 | <span class="headline"> |
80 | Neza 3 |
81 | </span> |
82 | <span class="supporting"> |
83 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
84 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
85 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
86 | alias! |
87 | </span> |
88 | </li> |
89 | |
90 | <li class="md-three-line video"> |
91 | <img alt="Ciudad de San Francisco" |
92 | src="img/pexels-craig-dennis-3701822.jpg"> |
93 | <span class="headline"> |
94 | San Francisco 3 |
95 | </span> |
96 | <span class="supporting"> |
97 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
98 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
99 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
100 | alias! |
101 | </span> |
102 | </li> |
103 | |
104 | <li> |
105 | <a class="md-three-line image" target="_blank" rel=”noreferrer” |
106 | href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/"> |
107 | <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg"> |
108 | <span class="headline"> |
109 | Neza Link 3 |
110 | </span> |
111 | <span class="supporting"> |
112 | Lorem ipsum dolor sit amet consectetur adipisicing elit. |
113 | Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque |
114 | illo amet, eos ea similique quia, maiores tenetur modi nobis expedita |
115 | alias! |
116 | </span> |
117 | </a> |
118 | </li> |
119 | |
120 | </ul> |
121 | |
122 | <footer> |
123 | <ul> |
124 | <li> |
125 | <p> |
126 | <small> |
127 | <a target="_blank" rel=”noreferrer” |
128 | href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/"> |
129 | La foto de la niña es de Moises Patrício, publicada en el sitio Pexels. |
130 | Haz clic en este hipervínculo para más información. |
131 | </a> |
132 | </small> |
133 | </p> |
134 | </li> |
135 | <li> |
136 | <p> |
137 | <small> |
138 | <a target="_blank" rel=”noreferrer” |
139 | href="https://www.pinterest.com.mx/ludresi/"> |
140 | La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio |
141 | Pinterest. Haz clic en este hipervínculo para más información. |
142 | </a> |
143 | </small> |
144 | </p> |
145 | </li> |
146 | <li> |
147 | <p> |
148 | <small> |
149 | <a target="_blank" rel=”noreferrer” |
150 | href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/"> |
151 | La foto del puente de San Francisco es de Craig Dennis, publicada en el |
152 | sitio Pexels. Haz clic en este hipervínculo para más información. |
153 | </a> |
154 | </small> |
155 | </p> |
156 | </li> |
157 | </ul> |
158 | </footer> |
159 | |
160 | </main> |
161 | |
162 | <nav-drw></nav-drw> |
163 | |
164 | </body> |
165 | |
166 | </html> |
Las listas están definida en https://m3.material.io/components/lists/overview.
Las listas de tarjetas deben usar un
div class="md-cards"
.
Los elementos de la lista de tarjetas pueden ser
div
,
span
o
a
Pueden llevar multimedia, que debe colocarse dentro de un elemento de tipo
figure
.
El texto de encabezado se marca con la class
headline
.
El texto de soporte se marca con la class
supporting
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Tarjetas - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar> |
26 | <h1>Tarjetas</h1> |
27 | |
28 | <button is="md-menu-button" slot="navigation"></button> |
29 | |
30 | </md-top-app-bar> |
31 | |
32 | <div class="md-cards"> |
33 | |
34 | <a target="_blank" rel=”noreferrer” |
35 | href="https://www.pexels.com/es-es/foto/lobo-blanco-y-negro-397857/"> |
36 | <figure> |
37 | <img alt="Lobo" src="img/pexels-steve-397857.jpg"> |
38 | </figure> |
39 | <span class="headline"> |
40 | Lobo |
41 | </span> |
42 | <span class="supporting"> |
43 | Foto de Steve en Pexels. |
44 | </span> |
45 | </a> |
46 | |
47 | <a target="_blank" rel=”noreferrer” |
48 | href="https://www.pexels.com/es-es/foto/foto-de-buho-ural-3732453/"> |
49 | <figure> |
50 | <img alt="Buho" src="img/pexels-erik-karits-3732453.jpg"> |
51 | </figure> |
52 | <span class="headline"> |
53 | Buho |
54 | </span> |
55 | <span class="supporting"> |
56 | Foto de Erik Karits en Pexels |
57 | </span> |
58 | </a> |
59 | |
60 | <a target="_blank" rel=”noreferrer” |
61 | href="https://www.pexels.com/es-es/foto/perro-de-pelo-corto-marron-y-blanco-acostado-3978352/"> |
62 | <figure> |
63 | <img alt="Perro" src="img/pexels-creative-workshop-3978352.jpg"> |
64 | </figure> |
65 | <span class="headline"> |
66 | Perro |
67 | </span> |
68 | <span class="supporting"> |
69 | Foto de Creative Workshop en Pexels |
70 | </span> |
71 | </a> |
72 | |
73 | <a target="_blank" rel=”noreferrer” |
74 | href="https://www.pexels.com/es-es/foto/gatito-gris-en-bolsa-de-papel-plateada-141496/"> |
75 | <figure> |
76 | <img alt="Gato" src="img/pexels-vadim-b-141496.jpg"> |
77 | </figure> |
78 | <span class="headline"> |
79 | Gato |
80 | </span> |
81 | <span class="supporting"> |
82 | Foto de Vadim B en Pexels |
83 | </span> |
84 | </a> |
85 | |
86 | <a target="_blank" rel=”noreferrer” |
87 | href="https://www.pexels.com/es-es/foto/leon-marron-2270848/"> |
88 | <figure> |
89 | <img alt="León" src="img/pexels-ralph-2270848.jpg"> |
90 | </figure> |
91 | <span class="headline"> |
92 | León |
93 | </span> |
94 | <span class="supporting"> |
95 | Foto de Ralph en Pexels |
96 | </span> |
97 | </a> |
98 | |
99 | <a target="_blank" rel=”noreferrer” |
100 | href="https://www.pexels.com/es-es/foto/oso-cafe-35435/"> |
101 | <figure> |
102 | <img alt="Oso" src="img/pexels-rasmus-svinding-35435.jpg"> |
103 | </figure> |
104 | <span class="headline"> |
105 | Oso |
106 | </span> |
107 | <span class="supporting"> |
108 | Foto de Rasmus Svinding en Pexels |
109 | </span> |
110 | </a> |
111 | |
112 | <a target="_blank" rel=”noreferrer” |
113 | href="https://www.pexels.com/es-es/foto/animal-perro-mono-hierba-10226903/"> |
114 | <figure> |
115 | <img alt="Coyote" src="img/pexels-esteban-arango-10226903.jpg"> |
116 | </figure> |
117 | <span class="headline"> |
118 | Coyote |
119 | </span> |
120 | <span class="supporting"> |
121 | Foto de Esteban Arango en Pexels |
122 | </span> |
123 | </a> |
124 | |
125 | </div> |
126 | |
127 | <nav-drw></nav-drw> |
128 | |
129 | </body> |
130 | |
131 | </html> |
Las tarjetas están definida en https://m3.material.io/components/cards/specs.
Se usa para seleccionar entre 5 o más vistas.
Se usa en:
Móvil
Tablet
Escritorio
Algunos diseñadores prefieren usar pestañas, barras de navegación o navigation rail.
Se crea el elemento personalizado
nav-drw
.
Puedes ver su código en
m-js/m-nav-drw-js.html
Coloca el botón
is="md-menu-button"
,
dentro del
md-top-app-bar
con
slot="navigation"
.
Coloca
nav-drw
hasta el final.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Vista secundaria - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar class="medium" headline="h1Headline"> |
26 | |
27 | <h1>Secundaria</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | <button type="button" class="md-standard-icon-button" title="Agregar" |
32 | slot="action"> |
33 | <span class="material-symbols-outlined">add</span> |
34 | </button> |
35 | |
36 | <button type="button" class="md-standard-icon-button" title="Editar" |
37 | slot="action"> |
38 | <span class="material-symbols-outlined">edit</span> |
39 | </button> |
40 | |
41 | <button is="md-overflow-button" slot="action" |
42 | onclick="overflow.alterna(this)"></button> |
43 | |
44 | </md-top-app-bar> |
45 | |
46 | <h1 id="h1Headline">Página secundaria</h1> |
47 | |
48 | <main> |
49 | <p> |
50 | Esta es una página secundaria de la app. Las X que siguen son para que veas |
51 | como se comporta cuando se hace scroll. |
52 | </p> |
53 | <p>x</p> |
54 | <p>x</p> |
55 | <p>x</p> |
56 | <p>x</p> |
57 | <p>x</p> |
58 | <p>x</p> |
59 | <p>x</p> |
60 | <p>x</p> |
61 | <p>x</p> |
62 | <p>x</p> |
63 | <p>x</p> |
64 | <p>x</p> |
65 | <p>x</p> |
66 | <p>x</p> |
67 | <p>x</p> |
68 | <p>x</p> |
69 | <p>x</p> |
70 | <p>x</p> |
71 | <p>x</p> |
72 | <p>x</p> |
73 | <p>x</p> |
74 | <p>x</p> |
75 | <p>x</p> |
76 | </main> |
77 | |
78 | <md-overflow-menu id="overflow"> |
79 | |
80 | <button type="button"> |
81 | <span class="material-symbols-outlined">star</span> |
82 | Marcar favorito |
83 | </button> |
84 | |
85 | <button type="button"> |
86 | <span class="material-symbols-outlined"> delete</span> |
87 | Eliminar |
88 | </button> |
89 | |
90 | </md-overflow-menu> |
91 | |
92 | <nav-drw></nav-drw> |
93 | |
94 | </body> |
95 | |
96 | </html> |
El cajón de navegación está definido en https://m3.material.io/components/navigation-drawer/overview.
El navigation rail está definido en https://m3.material.io/components/navigation-rail/overview.
Se usa para seleccionar entre 2 o más vistas.
Se usa en:
Móvil
Tablet
Escritorio
Se crea el elemento personalizado
nav-tab-scrollable
.
Puedes ver su código en
m-js/m-nav-tab-scrollable-js.html
Para reducir los problemas de despliegues transitorios conocidos como fuoc,
se añade la siguiente línea después de los link para estilos:
<link rel="expect" blocking="render" href="#navtab">
,
donde navtab
es el id de cualquier hiperenlace del elemento
personalizado
nav-tab-scrollable
.
Añade al
top-app-bar
el atributo
adicional
con el id para
nav-tab-scrollable
.
Coloca
nav-tab-scrollable
después de
top-app-bar
y de
h1
.
Ponle su
id
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Pestañas scrollable - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_pestanas.css"> |
20 | <link rel="expect" blocking="render" href="#navtab"> |
21 | |
22 | </head> |
23 | |
24 | <body> |
25 | |
26 | <md-top-app-bar adicional="tab" headline="headline"> |
27 | |
28 | <h1>scrollable</h1> |
29 | |
30 | <button type="button" class="md-standard-icon-button" title="Agregar" |
31 | slot="action"> |
32 | <span class="material-symbols-outlined">add</span> |
33 | </button> |
34 | <button type="button" class="md-standard-icon-button" title="Editar" |
35 | slot="action"> |
36 | <span class="material-symbols-outlined">edit</span> |
37 | </button> |
38 | |
39 | </md-top-app-bar> |
40 | |
41 | <h1 id="headline">Pestañas scrollable</h1> |
42 | |
43 | <nav-tab-scrollable id="tab"></nav-tab-scrollable> |
44 | |
45 | <main> |
46 | <p> |
47 | Esta página usa navegación por pestañas fijas. Las X que siguen son para que |
48 | veas como se comporta cuando se hace scroll. |
49 | </p> |
50 | <p>x</p> |
51 | <p>x</p> |
52 | <p>x</p> |
53 | <p>x</p> |
54 | <p>x</p> |
55 | <p>x</p> |
56 | <p>x</p> |
57 | <p>x</p> |
58 | <p>x</p> |
59 | <p>x</p> |
60 | <p>x</p> |
61 | <p>x</p> |
62 | <p>x</p> |
63 | <p>x</p> |
64 | <p>x</p> |
65 | <p>x</p> |
66 | <p>x</p> |
67 | <p>x</p> |
68 | <p>x</p> |
69 | <p>x</p> |
70 | <p>x</p> |
71 | <p>x</p> |
72 | <p>x</p> |
73 | </main> |
74 | |
75 | </body> |
76 | |
77 | </html> |
Las pestañas están definidas en https://m3.material.io/components/tabs/overview.
Cuando son pocas pestañas, pueden mantenerse fijas.
Se crea el elemento personalizado
nav-tab-fixed
.
Puedes ver su código en
m-js/m-nav-tab-fixed-js.html
Para reducir los problemas de despliegues transitorios conocidos como fuoc,
se añade la siguiente línea después de los link para estilos:
<link rel="expect" blocking="render" href="#navtabfixed">
,
donde navtabfixed
es el id de cualquier hiperenlace del elemento
personalizado
nav-tab-fixed
.
Añade al
top-app-bar
el atributo
adicional
con el id para
nav-tab-fixed
.
Coloca
nav-tab-fixed
después de
top-app-bar
y de
h1
.
Ponle su
id
.
Puedes cambiar el ancho de las pestañas cambiando la definición de
--tabWidth
en
estilos.css.
o redefine el valor de --tabWidth en el atributo
style
de la etiquta
nav-tab-fixed
.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Pestañas fijas - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_pestanas.css"> |
20 | <link rel="expect" blocking="render" href="#navtabfixed"> |
21 | |
22 | </head> |
23 | |
24 | <body> |
25 | |
26 | <md-top-app-bar adicional="tab" headline="headline"> |
27 | |
28 | <h1>fijas</h1> |
29 | |
30 | <button type="button" class="md-standard-icon-button" title="Agregar" |
31 | slot="action"> |
32 | <span class="material-symbols-outlined">add</span> |
33 | </button> |
34 | <button type="button" class="md-standard-icon-button" title="Editar" |
35 | slot="action"> |
36 | <span class="material-symbols-outlined">edit</span> |
37 | </button> |
38 | |
39 | </md-top-app-bar> |
40 | |
41 | <h1 id="headline">Pestañas fijas</h1> |
42 | |
43 | <nav-tab-fixed id="tab"></nav-tab-fixed> |
44 | |
45 | <main> |
46 | <p> |
47 | Esta página usa navegación por pestañas filas. Las X que siguen son para que |
48 | veas como se comporta cuando se hace scroll. |
49 | </p> |
50 | <p>x</p> |
51 | <p>x</p> |
52 | <p>x</p> |
53 | <p>x</p> |
54 | <p>x</p> |
55 | <p>x</p> |
56 | <p>x</p> |
57 | <p>x</p> |
58 | <p>x</p> |
59 | <p>x</p> |
60 | <p>x</p> |
61 | <p>x</p> |
62 | <p>x</p> |
63 | <p>x</p> |
64 | <p>x</p> |
65 | <p>x</p> |
66 | <p>x</p> |
67 | <p>x</p> |
68 | <p>x</p> |
69 | <p>x</p> |
70 | <p>x</p> |
71 | <p>x</p> |
72 | <p>x</p> |
73 | </main> |
74 | |
75 | </body> |
76 | |
77 | </html> |
Se usa para seleccionar entre 3 a 5 vistas.
Se usa en:
Móvil
Aunque no lo especifica Material Design, cuando el número de vistas es mayor a 5, algunos diseñadores prefieren seguir usando una barra de navegación y convertir el último botón en un cajón de navegación para las vistas restantes.
En tablet y móvil se convierte en un menú lateral o en un navigation reel.
Se crea el elemento personalizado
nav-bar
.
Usa textos cortos. Puedes ver su código en
m-js/m-nav-bar-js.html
Para reducir los problemas de despliegues transitorios conocidos como fuoc,
se añade la siguiente línea después de los link para estilos:
<link rel="expect" blocking="render" href="#navbar">
,
donde navbar
es el id de cualquier hiperenlace del elemento
personalizado
nav-bar
.
Coloca
nav-bar
al final de tu forma.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Barra de Navegación - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_pestanas.css"> |
20 | <link rel="expect" blocking="render" href="#navbar"> |
21 | |
22 | </head> |
23 | |
24 | <body |
25 | style="padding-bottom: 5rem;"> |
26 | |
27 | <md-top-app-bar headline="headline"> |
28 | |
29 | <h1>navegación</h1> |
30 | |
31 | <button type="button" class="md-standard-icon-button" title="Agregar" |
32 | slot="action"> |
33 | <span class="material-symbols-outlined">add</span> |
34 | </button> |
35 | <button type="button" class="md-standard-icon-button" title="Editar" |
36 | slot="action"> |
37 | <span class="material-symbols-outlined">edit</span> |
38 | </button> |
39 | |
40 | </md-top-app-bar> |
41 | |
42 | <h1 id="headline">Barra de navegación</h1> |
43 | |
44 | <main> |
45 | <p> |
46 | Esta página usa barra de navegación. Las X que siguen son para que veas como |
47 | se comporta cuando se hace scroll. |
48 | </p> |
49 | <p>x</p> |
50 | <p>x</p> |
51 | <p>x</p> |
52 | <p>x</p> |
53 | <p>x</p> |
54 | <p>x</p> |
55 | <p>x</p> |
56 | <p>x</p> |
57 | <p>x</p> |
58 | <p>x</p> |
59 | <p>x</p> |
60 | <p>x</p> |
61 | <p>x</p> |
62 | <p>x</p> |
63 | <p>x</p> |
64 | <p>x</p> |
65 | <p>x</p> |
66 | <p>x</p> |
67 | <p>x</p> |
68 | <p>x</p> |
69 | <p>x</p> |
70 | <p>x</p> |
71 | <p>x</p> |
72 | </main> |
73 | |
74 | <nav-bar></nav-bar> |
75 | |
76 | </body> |
77 | |
78 | </html> |
La barra de navegación está definida en https://m3.material.io/components/navigation-bar/overview.
Los mensajes de error de un componente, se muestran en otro conocido como
texto de ayuda, que normalmente usa un elemento
small
con el atributo
slot="supporting"
.
La forma usa el atributo
novalidate
.
En este ejemplo, el campo
select-menu
con el id
selectGenero
usa el evento
input
.
para invocar la función
copiaMensajes
.
y copiar los mensajes de error del campo de captura al texto de ayuda.
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Formulario - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <form id="form" novalidate onsubmit="procesa(event)"> |
26 | |
27 | <md-top-app-bar> |
28 | |
29 | <h1>Formulario</h1> |
30 | |
31 | <button is="md-menu-button" slot="navigation"></button> |
32 | |
33 | </md-top-app-bar> |
34 | |
35 | <main> |
36 | |
37 | <p> |
38 | <span class="md-filled-text-field" accesskey="G"> |
39 | <md-select-menu id="selectGenero" required options="opcionesDeGenero" |
40 | oninput="copiaMensajes()"></md-select-menu> |
41 | <span>Género *</span> |
42 | <small id="supportingGenero">Obligatorio</small> |
43 | </span> |
44 | </p> |
45 | |
46 | <p> |
47 | <button class="md-filled-button" style="width: 100%;">Recomendar</button> |
48 | </p> |
49 | |
50 | </main> |
51 | |
52 | <md-options-menu id="opcionesDeGenero" aria-label="Opciones de género"> |
53 | <span data-value="" title="Selecciona género"></span> |
54 | <span data-value="pop">Pop</span> |
55 | <span data-value="reg">Reguetón</span> |
56 | </md-options-menu> |
57 | |
58 | <nav-drw></nav-drw> |
59 | |
60 | </form> |
61 | |
62 | <script type="module"> |
63 | import { muestraTextoDeAyuda } from "./lib/js/muestraTextoDeAyuda.js" |
64 | import { exportaAHtml } from "./lib/js/exportaAHtml.js" |
65 | import { muestraError } from "./lib/js/muestraError.js" |
66 | |
67 | function copiaMensajes() { |
68 | muestraTextoDeAyuda(selectGenero, supportingGenero, "Obligatorio") |
69 | } |
70 | exportaAHtml(copiaMensajes) |
71 | |
72 | /** |
73 | * @param {SubmitEvent} evt |
74 | */ |
75 | function procesa(evt) { |
76 | evt.preventDefault() |
77 | try { |
78 | copiaMensajes() |
79 | if ( |
80 | selectGenero.validity.valid) { |
81 | const genero = selectGenero.value |
82 | const resultado = recomienda(genero) |
83 | alert(resultado) |
84 | } |
85 | } catch (e) { |
86 | muestraError(e) |
87 | } |
88 | } |
89 | exportaAHtml(procesa) |
90 | |
91 | /** @param {string} genero */ |
92 | function recomienda(genero) { |
93 | if (genero === "pop") { |
94 | return "Para el pop te recomiendo a Dua Lipa." |
95 | } else if (genero === "reg") { |
96 | return "Para el reguetón te recomiendo a Bad Bunny." |
97 | } |
98 | } |
99 | </script> |
100 | |
101 | </body> |
102 | |
103 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es" class="light dark"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <title>Ayuda - PWA con MD</title> |
8 | |
9 | <script src="js/registraServiceWorker.js"></script> |
10 | |
11 | <meta name="viewport" content="width=device-width"> |
12 | <meta name="theme-color" content="#fffbfe"> |
13 | <link rel="icon" sizes="32x32" href="favicon.ico"> |
14 | <link rel="manifest" href="site.webmanifest"> |
15 | <script src="ungap/custom-elements.js"></script> |
16 | |
17 | <script type="module" src="js/configura.js"></script> |
18 | <link rel="stylesheet" href="css/estilos.css"> |
19 | <link rel="stylesheet" href="css/transicion_completa.css"> |
20 | |
21 | </head> |
22 | |
23 | <body> |
24 | |
25 | <md-top-app-bar> |
26 | |
27 | <h1>Ayuda</h1> |
28 | |
29 | <button is="md-menu-button" slot="navigation"></button> |
30 | |
31 | </md-top-app-bar> |
32 | |
33 | <main> |
34 | |
35 | <ul class="md-list"> |
36 | <li class="md-two-line"> |
37 | <span class="headline"> |
38 | Título |
39 | </span> |
40 | <span class="supporting"> |
41 | PWA con Material Design |
42 | </span> |
43 | </li> |
44 | <li class="md-two-line"> |
45 | <span class="headline"> |
46 | Descripción |
47 | </span> |
48 | <span class="supporting"> |
49 | Ejemplos de vistas móviles. |
50 | </span> |
51 | </li> |
52 | <li class="md-two-line"> |
53 | <span class="headline"> |
54 | Autor |
55 | </span> |
56 | <span class="supporting"> |
57 | Gilberto Pacheco Gallegos |
58 | </span> |
59 | </li> |
60 | <li class="md-two-line"> |
61 | <span class="headline"> |
62 | Derechos de autor |
63 | </span> |
64 | <span class="supporting"> |
65 | © 2024 Gilberto Pacheco Gallegos |
66 | </span> |
67 | </li> |
68 | <li class="md-three-line"> |
69 | <span class="headline"> |
70 | Este software usa la librería para PWA |
71 | </span> |
72 | <span class="supporting"> |
73 | Esta obra de Gilberto Pacheco Gallegos está bajo una |
74 | <a target="_blank" rel="license noreferrer" |
75 | href="http://creativecommons.org/licenses/by/4.0/"> |
76 | Licencia Creative Commons Atribución 4.0 Internacional</a></span> |
77 | </li> |
78 | <li> |
79 | <a class="md-three-line" target="_blank" rel=”noreferrer” |
80 | href="https://fonts.google.com/icons"> |
81 | <span class="headline"> |
82 | También usa Material Symbols |
83 | </span> |
84 | <span class="supporting"> |
85 | Desarrollada por Google bajo licencia Apache 2.0 |
86 | </span> |
87 | </a> |
88 | </li> |
89 | </ul> |
90 | |
91 | </main> |
92 | |
93 | <nav-drw></nav-drw> |
94 | |
95 | </body> |
96 | |
97 | </html> |
1 | /** Barra de navegación. */ |
2 | import "./nav-drw.js" |
3 | import "./nav-tab-scrollable.js" |
4 | import "./nav-tab-fixed.js" |
5 | import "./nav-bar.js" |
6 | /** Elementos utilizados */ |
7 | import "../lib/js/custom/md-menu-button.js" |
8 | import "../lib/js/custom/md-options-menu.js" |
9 | import "../lib/js/custom/md-overflow-button.js" |
10 | import "../lib/js/custom/md-overflow-menu.js" |
11 | import "../lib/js/custom/md-select-menu.js" |
12 | import "../lib/js/custom/md-top-app-bar.js" |
13 | import "../lib/js/custom/md-slider-field.js" |
1 | import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js" |
2 | |
3 | export class NavBar extends HTMLElement { |
4 | |
5 | connectedCallback() { |
6 | this.classList.add("md-navigation-bar") |
7 | |
8 | this.innerHTML = /* HTML */` |
9 | <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html"> |
10 | <span class="material-symbols-outlined">home</span> |
11 | Inicio |
12 | </a> |
13 | |
14 | <a ${resaltaSiEstasEn(["/navTabFixed.html"])} href="navTabFixed.html"> |
15 | <span class="material-symbols-outlined">tabs</span> |
16 | Pestañas |
17 | </a> |
18 | |
19 | <a id="navbar" ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html"> |
20 | <span class="material-symbols-outlined">bottom_navigation</span> |
21 | Barra |
22 | </a> |
23 | |
24 | <a ${resaltaSiEstasEn(["/formulario.html"])} href="formulario.html"> |
25 | <span class="material-symbols-outlined">newspaper</span> |
26 | Forma |
27 | </a>` |
28 | |
29 | } |
30 | |
31 | } |
32 | |
33 | customElements.define("nav-bar", NavBar) |
1 | import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js" |
2 | import { MdNavigationDrawer } from "../lib/js/custom/MdNavigationDrawer.js" |
3 | |
4 | export class NavDrw extends MdNavigationDrawer { |
5 | |
6 | /** |
7 | * @override |
8 | */ |
9 | getHipervinculos() { |
10 | return /* HTML */` |
11 | <h1>PWA con MD</h1> |
12 | |
13 | <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html"> |
14 | <span class="material-symbols-outlined">home</span> |
15 | Inicio |
16 | </a> |
17 | |
18 | <a ${resaltaSiEstasEn(["/secundaria.html"])} href="secundaria.html"> |
19 | <span class="material-symbols-outlined">scrollable_header</span> |
20 | Página secundaria |
21 | </a> |
22 | |
23 | <a ${resaltaSiEstasEn(["/iconos.html"])} href="iconos.html"> |
24 | <span class="material-symbols-outlined">sentiment_satisfied</span> |
25 | Íconos |
26 | </a> |
27 | |
28 | <a ${resaltaSiEstasEn(["/botones.html"])} href="botones.html"> |
29 | <span class="material-symbols-outlined">right_click</span> |
30 | Botones |
31 | </a> |
32 | |
33 | <a ${resaltaSiEstasEn(["/campos.html"])} href="campos.html"> |
34 | <span class="material-symbols-outlined">password</span> |
35 | Campos de texto |
36 | </a> |
37 | |
38 | <a ${resaltaSiEstasEn(["/select.html"])} href="select.html"> |
39 | <span class="material-symbols-outlined">bottom_panel_close</span> |
40 | Select |
41 | </a> |
42 | |
43 | <a ${resaltaSiEstasEn(["/interruptor.html"])} href="interruptor.html"> |
44 | <span class="material-symbols-outlined">toggle_on</span> |
45 | Interruptores |
46 | </a> |
47 | |
48 | <a ${resaltaSiEstasEn(["/slider.html"])} href="slider.html"> |
49 | <span class="material-symbols-outlined">linear_scale</span> |
50 | Sliders |
51 | </a> |
52 | |
53 | <a ${resaltaSiEstasEn(["/segmentado.html"])} href="segmentado.html"> |
54 | <span class="material-symbols-outlined">splitscreen_left</span> |
55 | Botón segmentado |
56 | </a> |
57 | |
58 | <a ${resaltaSiEstasEn(["/one-line.html"])} href="one-line.html"> |
59 | <span class="material-symbols-outlined">list</span> |
60 | Listas one-line |
61 | </a> |
62 | |
63 | <a ${resaltaSiEstasEn(["/two-line.html"])} href="two-line.html"> |
64 | <span class="material-symbols-outlined">lists</span> |
65 | Listas two-line |
66 | </a> |
67 | |
68 | <a ${resaltaSiEstasEn(["/three-line.html"])} href="three-line.html"> |
69 | <span class="material-symbols-outlined">receipt_long</span> |
70 | Listas three-line |
71 | </a> |
72 | |
73 | <a ${resaltaSiEstasEn(["/tarjetas.html"])} href="tarjetas.html"> |
74 | <span class="material-symbols-outlined">cards</span> |
75 | Tarjetas |
76 | </a> |
77 | |
78 | <a ${resaltaSiEstasEn(["/navtab.html"])} href="navtab.html"> |
79 | <span class="material-symbols-outlined">swipe_left</span> |
80 | Pestañas scrollable |
81 | </a> |
82 | |
83 | <a ${resaltaSiEstasEn(["/navTabFixed.html"])} href="navTabFixed.html"> |
84 | <span class="material-symbols-outlined">tabs</span> |
85 | Pestañas fijas |
86 | </a> |
87 | |
88 | <a ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html"> |
89 | <span class="material-symbols-outlined">bottom_navigation</span> |
90 | Barra de navegación |
91 | </a> |
92 | |
93 | <a ${resaltaSiEstasEn(["/formulario.html"])} href="formulario.html"> |
94 | <span class="material-symbols-outlined">newspaper</span> |
95 | Formulario |
96 | </a> |
97 | |
98 | <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html"> |
99 | <span class="material-symbols-outlined">help</span> |
100 | Ayuda |
101 | </a>` |
102 | } |
103 | |
104 | } |
105 | |
106 | customElements.define("nav-drw", NavDrw) |
1 | import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js" |
2 | |
3 | export class NavTabFixed extends HTMLElement { |
4 | |
5 | connectedCallback() { |
6 | this.classList.add("md-tab", "fixed") |
7 | |
8 | this.innerHTML = /* HTML */` |
9 | <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html"> |
10 | <span class="material-symbols-outlined">home</span> |
11 | Inicio |
12 | </a> |
13 | |
14 | <a ${resaltaSiEstasEn(["/navtab.html"])} href="navtab.html"> |
15 | <span class="material-symbols-outlined">swipe_left</span> |
16 | Pestañas scrollable |
17 | </a> |
18 | |
19 | <a id="navtabfixed" ${resaltaSiEstasEn(["/navTabFixed.html"])} |
20 | href="navTabFixed.html"> |
21 | <span class="material-symbols-outlined">tabs</span> |
22 | Pestañas fijas |
23 | </a> |
24 | |
25 | <a ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html"> |
26 | <span class="material-symbols-outlined">bottom_navigation</span> |
27 | Barra de navegación |
28 | </a>` |
29 | } |
30 | |
31 | } |
32 | |
33 | customElements.define("nav-tab-fixed", NavTabFixed) |
1 | import { querySelector } from "../lib/js/querySelector.js" |
2 | import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js" |
3 | |
4 | export class NavTabScrollable extends HTMLElement { |
5 | |
6 | connectedCallback() { |
7 | this.classList.add("md-tab", "scrollable") |
8 | |
9 | this.innerHTML = /* HTML */` |
10 | <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html"> |
11 | <span class="material-symbols-outlined">home</span> |
12 | Inicio |
13 | </a> |
14 | |
15 | <a ${resaltaSiEstasEn(["/secundaria.html"])} href="secundaria.html"> |
16 | <span class="material-symbols-outlined">scrollable_header</span> |
17 | Página secundaria |
18 | </a> |
19 | |
20 | <a ${resaltaSiEstasEn(["/iconos.html"])} href="iconos.html"> |
21 | <span class="material-symbols-outlined">sentiment_satisfied</span> |
22 | Íconos |
23 | </a> |
24 | |
25 | <a ${resaltaSiEstasEn(["/botones.html"])} href="botones.html"> |
26 | <span class="material-symbols-outlined">right_click</span> |
27 | Botones |
28 | </a> |
29 | |
30 | <a ${resaltaSiEstasEn(["/campos.html"])} href="campos.html"> |
31 | <span class="material-symbols-outlined">password</span> |
32 | Campos de texto |
33 | </a> |
34 | |
35 | <a ${resaltaSiEstasEn(["/select.html"])} href="select.html"> |
36 | <span class="material-symbols-outlined">bottom_panel_close</span> |
37 | Select |
38 | </a> |
39 | |
40 | <a ${resaltaSiEstasEn(["/interruptor.html"])} href="interruptor.html"> |
41 | <span class="material-symbols-outlined">toggle_on</span> |
42 | Interruptores |
43 | </a> |
44 | |
45 | <a ${resaltaSiEstasEn(["/slider.html"])} href="slider.html"> |
46 | <span class="material-symbols-outlined">linear_scale</span> |
47 | Sliders |
48 | </a> |
49 | |
50 | <a ${resaltaSiEstasEn(["/segmentado.html"])} href="segmentado.html"> |
51 | <span class="material-symbols-outlined">splitscreen_left</span> |
52 | Botón segmentado |
53 | </a> |
54 | |
55 | <a ${resaltaSiEstasEn(["/one-line.html"])} href="one-line.html"> |
56 | <span class="material-symbols-outlined">list</span> |
57 | Listas one-line |
58 | </a> |
59 | |
60 | <a ${resaltaSiEstasEn(["/two-line.html"])} href="two-line.html"> |
61 | <span class="material-symbols-outlined">lists</span> |
62 | Listas two-line |
63 | </a> |
64 | |
65 | <a ${resaltaSiEstasEn(["/three-line.html"])} href="three-line.html"> |
66 | <span class="material-symbols-outlined">receipt_long</span> |
67 | Listas three-line |
68 | </a> |
69 | |
70 | <a ${resaltaSiEstasEn(["/tarjetas.html"])} href="tarjetas.html"> |
71 | <span class="material-symbols-outlined">cards</span> |
72 | Tarjetas |
73 | </a> |
74 | |
75 | <a id="navtab" ${resaltaSiEstasEn(["/navtab.html"])} href="navtab.html"> |
76 | <span class="material-symbols-outlined">swipe_left</span> |
77 | Pestañas scrollable |
78 | </a> |
79 | |
80 | <a ${resaltaSiEstasEn(["/navTabFixed.html"])} href="navTabFixed.html"> |
81 | <span class="material-symbols-outlined">tabs</span> |
82 | Pestañas fijas |
83 | </a> |
84 | |
85 | <a ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html"> |
86 | <span class="material-symbols-outlined">bottom_navigation</span> |
87 | Barra de navegación |
88 | </a> |
89 | |
90 | <a ${resaltaSiEstasEn(["/formulario.html"])} href="formulario.html"> |
91 | <span class="material-symbols-outlined">newspaper</span> |
92 | Formulario |
93 | </a> |
94 | |
95 | <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html"> |
96 | <span class="material-symbols-outlined">help</span> |
97 | Ayuda |
98 | </a>` |
99 | |
100 | } |
101 | |
102 | } |
103 | |
104 | customElements.define("nav-tab-scrollable", NavTabScrollable) |
1 | "use strict" // usa JavaScript en modo estricto. |
2 | |
3 | const nombreDeServiceWorker = "sw.js" |
4 | |
5 | try { |
6 | navigator.serviceWorker.register(nombreDeServiceWorker) |
7 | .then(registro => { |
8 | console.log(nombreDeServiceWorker, "registrado.") |
9 | console.log(registro) |
10 | }) |
11 | .catch(error => console.log(error)) |
12 | } catch (error) { |
13 | console.log(error) |
14 | } |
Este archivo sirve para configurar los instaladores de la aplicación.
short_name
Nombre corto. Normalmente se despliega en dispositivos móviles. Máximo 20 caracteres.
name
Nombre largo. Normalmente se despliega en computadoras de escritorio. Máximo 30 caracteres.
id
Identificador del archivo de instalación. Normalmente es la ruta del archivo inicial de la app.
start_url
Ruta del archivo inicial de la app.
display
Forma de mostrar la app. El término standalone
significa que no
se muestra la barra de navegación del navegador web.
theme_color
Color de la barra de estado (en dispositivos móviles) o de título (en computadoras de escritorio) de la app.
background_color
Color de fondo de la pantalla desplah en dispositivos móviles.
description
Describe el propósito de la aplicación. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.
screenshots
Listado de máximo 8 capturas de pantalla. Aparecen en el cuadro de diálogo
que muestra el navegador al instalar la app. Debes incluir al menos una con
"form_factor": "wide"
y otra con
"form_factor": "narrow"
.
icons
Listado de íconos en distintas resoluciones para los instaladores de la app. Se selecciona el que se vea mejor según las característocas del dispositivo.
src
Url de la imagen dentro de la app.
sizes
Dimensiones en pixeles de la imagen, anchoxalto.
type
Tipo mime de la imagen.
purpose
Forma en que se usa la imagen.
maskable
La imagen puede recortarse de forma segura para tomar distintas formas, como círculos, gotas, cuadrados con esquinas redondeadas, etc. Normalmente se usa para dispositivos móviles.
any
No se puede asegurar nada sobre la imagen. Normalmente se usa para dispositivos de escritorio.
Normalmente debe proporcionarse un juego de íconos con purpose
any
y otro juego de íconos con purpose
maskable
.
form_factor
Orientación de una screenshot.
wide
La screenshot tiene una orientación horizontal. Normalmente la creenshot se usa para dispositivos de escritorio.
narrow
La screenshot tiene una orientación vertical. Normalmente la creenshot se usa para dispositivos móviles.
Debes incluir al menos una screenshot con
"form_factor": "wide"
y otra con
"form_factor": "narrow"
.
label
Descripción de una screenshot. Aparece en el cuadro de diálogo que muestra el navegador al instalar la app.
1 | { |
2 | "short_name": "PWA con MD", |
3 | "name": "PWA con Material Design", |
4 | "id": "/index.html", |
5 | "start_url": "/index.html", |
6 | "display": "standalone", |
7 | "theme_color": "#fffbfe", |
8 | "background_color": "#fffbfe", |
9 | "display_override": [ |
10 | "window-controls-overlay" |
11 | ], |
12 | "description": "PWA con componentes de Material Design.", |
13 | "screenshots": [ |
14 | { |
15 | "src": "/img/screenshot_horizontal.png", |
16 | "sizes": "1257x646", |
17 | "type": "image/png", |
18 | "form_factor": "wide", |
19 | "label": "PWA con Material Design" |
20 | }, |
21 | { |
22 | "src": "/img/screenshot_vertical.png", |
23 | "sizes": "595x644", |
24 | "type": "image/png", |
25 | "form_factor": "narrow", |
26 | "label": "PWA con Material Design (2)" |
27 | } |
28 | ], |
29 | "icons": [ |
30 | { |
31 | "src": "/img/maskable_icon_x48.png", |
32 | "sizes": "48x48", |
33 | "type": "image/png", |
34 | "purpose": "any" |
35 | }, |
36 | { |
37 | "src": "/img/maskable_icon_x72.png", |
38 | "sizes": "72x72", |
39 | "type": "image/png", |
40 | "purpose": "any" |
41 | }, |
42 | { |
43 | "src": "/img/maskable_icon_x96.png", |
44 | "sizes": "96x96", |
45 | "type": "image/png", |
46 | "purpose": "maskable" |
47 | }, |
48 | { |
49 | "src": "/img/maskable_icon_x128.png", |
50 | "sizes": "128x128", |
51 | "type": "image/png", |
52 | "purpose": "any" |
53 | }, |
54 | { |
55 | "src": "/img/maskable_icon_x192.png", |
56 | "sizes": "192x192", |
57 | "type": "image/png", |
58 | "purpose": "any" |
59 | }, |
60 | { |
61 | "src": "/img/maskable_icon_x384.png", |
62 | "sizes": "384x384", |
63 | "type": "image/png", |
64 | "purpose": "any" |
65 | }, |
66 | { |
67 | "src": "/img/maskable_icon_x512.png", |
68 | "sizes": "512x512", |
69 | "type": "image/png", |
70 | "purpose": "any" |
71 | }, |
72 | { |
73 | "src": "/img/maskable_icon.png", |
74 | "sizes": "3413x3413", |
75 | "type": "image/png", |
76 | "purpose": "any" |
77 | }, |
78 | { |
79 | "src": "/img/icono2048.png", |
80 | "sizes": "2048x2048", |
81 | "type": "image/png", |
82 | "purpose": "any" |
83 | }, |
84 | { |
85 | "src": "/img/maskable_icon_x48.png", |
86 | "sizes": "48x48", |
87 | "type": "image/png", |
88 | "purpose": "maskable" |
89 | }, |
90 | { |
91 | "src": "/img/maskable_icon_x72.png", |
92 | "sizes": "72x72", |
93 | "type": "image/png", |
94 | "purpose": "maskable" |
95 | }, |
96 | { |
97 | "src": "/img/maskable_icon_x96.png", |
98 | "sizes": "96x96", |
99 | "type": "image/png", |
100 | "purpose": "maskable" |
101 | }, |
102 | { |
103 | "src": "/img/maskable_icon_x128.png", |
104 | "sizes": "128x128", |
105 | "type": "image/png", |
106 | "purpose": "maskable" |
107 | }, |
108 | { |
109 | "src": "/img/maskable_icon_x192.png", |
110 | "sizes": "192x192", |
111 | "type": "image/png", |
112 | "purpose": "maskable" |
113 | }, |
114 | { |
115 | "src": "/img/maskable_icon_x384.png", |
116 | "sizes": "384x384", |
117 | "type": "image/png", |
118 | "purpose": "maskable" |
119 | }, |
120 | { |
121 | "src": "/img/maskable_icon_x512.png", |
122 | "sizes": "512x512", |
123 | "type": "image/png", |
124 | "purpose": "maskable" |
125 | }, |
126 | { |
127 | "src": "/img/maskable_icon.png", |
128 | "sizes": "3413x3413", |
129 | "type": "image/png", |
130 | "purpose": "maskable" |
131 | } |
132 | ] |
133 | } |
1 | Generar el listado de archivos del sw.js desde Visual Studio Code. |
2 | 1. Abrir una terminal desde el menú |
3 | Terminal > New Terminal |
4 | |
5 | 2. Desde la terminal introducir la orden: |
6 | Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt |
7 | |
8 | 3. Abrir el archivo generado, que se llama |
9 | archivos.txt |
10 | y sobre este, realizar los pasos que siguen: |
11 | |
12 | 4. Quita del archivo archivos.txt: |
13 | * el encabezado, |
14 | * todas las carpetas, |
15 | * el archivo .vscode/settings.json, |
16 | * el archivo .htaccess, |
17 | * el archivo archivos.txt, |
18 | * este archivo (instruccionesListadoSw.txt), |
19 | * el archivo jsconfig.json, |
20 | * el archivo sw.js, |
21 | * el archivo de la base de datos, que termina en ".bd" y |
22 | está en la carpeta srv, |
23 | * todos los archivos de php y |
24 | * las líneas en blanco del final |
25 | |
26 | 5. Cambia los \ por / desde Visual Studio Code con las siguientes |
27 | combinaciones de teclas: |
28 | |
29 | Ctrl+H En el diálogo que aparece introduce lo siguiente: |
30 | Find:\ |
31 | Replace:/ |
32 | |
33 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
34 | |
35 | 6. Coloca las comillas y coma del final de cada línea desde Visual |
36 | Studio Code con las siguientes combinaciones de teclas: |
37 | |
38 | Ctrl+H En el diálogo que aparece, selecciona el botón |
39 | ".*" |
40 | e introduce lo siguiente: |
41 | Find:\s*$ |
42 | Replace:", |
43 | |
44 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
45 | |
46 | 7. Marca la carpeta inicial, presiona la combinación de teclas: |
47 | |
48 | Shift+Ctrl+L |
49 | |
50 | borra la selección, teclea " y luego ESC |
51 | |
52 | 8. Cambia las secuencias de espacios por / con las siguientes |
53 | combinaciones de teclas: |
54 | |
55 | Ctrl+H En el diálogo que aparece, selecciona el botón |
56 | ".*" |
57 | e introduce lo siguiente: |
58 | Find:\s+ |
59 | Replace:/ |
60 | |
61 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
62 | |
63 | 9. Cambia las "/ por " con las siguientes combinaciones de teclas: |
64 | |
65 | Ctrl+H En el diálogo que aparece, quita la selección del botón |
66 | ".*" |
67 | e introduce lo siguiente: |
68 | Find:"/ |
69 | Replace:" |
70 | |
71 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
72 | |
73 | 10. Copia el texto al archivo |
74 | sw.js |
75 | en el contenido del arreglo llamado ARCHIVOS, pero recuerda |
76 | mantener el último elemento, que dice: |
77 | "/" |
1 | "ayuda.html", |
2 | "botones.html", |
3 | "campos.html", |
4 | "favicon.ico", |
5 | "formulario.html", |
6 | "iconos.html", |
7 | "index.html", |
8 | "interruptor.html", |
9 | "navbar.html", |
10 | "navtab.html", |
11 | "navTabFixed.html", |
12 | "one-line.html", |
13 | "secundaria.html", |
14 | "segmentado.html", |
15 | "select.html", |
16 | "site.webmanifest", |
17 | "slider.html", |
18 | "tarjetas.html", |
19 | "three-line.html", |
20 | "two-line.html", |
21 | "css/dark-hc.css", |
22 | "css/dark-mc.css", |
23 | "css/dark.css", |
24 | "css/estilos.css", |
25 | "css/light-hc.css", |
26 | "css/light-mc.css", |
27 | "css/light.css", |
28 | "css/transicion_completa.css", |
29 | "css/transicion_pestanas.css", |
30 | "img/Escultura_de_coyote.jpeg", |
31 | "img/icono2048.png", |
32 | "img/maskable_icon.png", |
33 | "img/maskable_icon_x128.png", |
34 | "img/maskable_icon_x192.png", |
35 | "img/maskable_icon_x384.png", |
36 | "img/maskable_icon_x48.png", |
37 | "img/maskable_icon_x512.png", |
38 | "img/maskable_icon_x72.png", |
39 | "img/maskable_icon_x96.png", |
40 | "img/pexels-craig-dennis-3701822.jpg", |
41 | "img/pexels-creative-workshop-3978352.jpg", |
42 | "img/pexels-erik-karits-3732453.jpg", |
43 | "img/pexels-esteban-arango-10226903.jpg", |
44 | "img/pexels-moises-patrício-10961948.jpg", |
45 | "img/pexels-ralph-2270848.jpg", |
46 | "img/pexels-rasmus-svinding-35435.jpg", |
47 | "img/pexels-steve-397857.jpg", |
48 | "img/pexels-vadim-b-141496.jpg", |
49 | "img/screenshot_horizontal.png", |
50 | "img/screenshot_vertical.png", |
51 | "js/configura.js", |
52 | "js/nav-bar.js", |
53 | "js/nav-drw.js", |
54 | "js/nav-tab-fixed.js", |
55 | "js/nav-tab-scrollable.js", |
56 | "js/registraServiceWorker.js", |
57 | "lib/css/material-symbols-outlined.css", |
58 | "lib/css/md-cards.css", |
59 | "lib/css/md-fab-primary.css", |
60 | "lib/css/md-filled-button.css", |
61 | "lib/css/md-filled-text-field.css", |
62 | "lib/css/md-list.css", |
63 | "lib/css/md-menu.css", |
64 | "lib/css/md-navigation-bar.css", |
65 | "lib/css/md-outline-button.css", |
66 | "lib/css/md-ripple.css", |
67 | "lib/css/md-segmented-button.css", |
68 | "lib/css/md-slider-field.css", |
69 | "lib/css/md-standard-icon-button.css", |
70 | "lib/css/md-switch.css", |
71 | "lib/css/md-tab.css", |
72 | "lib/css/md-top-app-bar.css", |
73 | "lib/css/roboto.css", |
74 | "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints", |
75 | "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf", |
76 | "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2", |
77 | "lib/fonts/roboto-v32-latin-regular.woff2", |
78 | "lib/js/abreElementoHtml.js", |
79 | "lib/js/cierraElementoHtmo.js", |
80 | "lib/js/exportaAHtml.js", |
81 | "lib/js/getAttribute.js", |
82 | "lib/js/htmlentities.js", |
83 | "lib/js/muestraError.js", |
84 | "lib/js/muestraTextoDeAyuda.js", |
85 | "lib/js/ProblemDetails.js", |
86 | "lib/js/querySelector.js", |
87 | "lib/js/resaltaSiEstasEn.js", |
88 | "lib/js/const/ES_APPLE.js", |
89 | "lib/js/custom/md-menu-button.js", |
90 | "lib/js/custom/md-options-menu.js", |
91 | "lib/js/custom/md-overflow-button.js", |
92 | "lib/js/custom/md-overflow-menu.js", |
93 | "lib/js/custom/md-select-menu.js", |
94 | "lib/js/custom/md-slider-field.js", |
95 | "lib/js/custom/md-top-app-bar.js", |
96 | "lib/js/custom/MdNavigationDrawer.js", |
97 | "material-tokens/css/baseline.css", |
98 | "material-tokens/css/colors.css", |
99 | "material-tokens/css/elevation.css", |
100 | "material-tokens/css/motion.css", |
101 | "material-tokens/css/palette.css", |
102 | "material-tokens/css/shape.css", |
103 | "material-tokens/css/state.css", |
104 | "material-tokens/css/typography.css", |
105 | "material-tokens/css/theme/dark.css", |
106 | "material-tokens/css/theme/light.css", |
107 | "ungap/custom-elements.js", |
1 | /* Este archivo debe estar colocado en la carpeta raíz del sitio. |
2 | * |
3 | * Cualquier cambio en el contenido de este archivo hace que el service |
4 | * worker se reinstale. */ |
5 | |
6 | /** |
7 | * Cambia el número de la versión cuando cambia el contenido de los |
8 | * archivos. |
9 | * |
10 | * El número a la izquierda del punto (.), en este caso <q>1</q>, se |
11 | * conoce como número mayor y se cambia cuando se realizan |
12 | * modificaciones grandes o importantes. |
13 | * |
14 | * El número a la derecha del punto (.), en este caso <q>00</q>, se |
15 | * conoce como número menor y se cambia cuando se realizan |
16 | * modificaciones menores. |
17 | */ |
18 | const VERSION = "1.00" |
19 | |
20 | /** |
21 | * Nombre de la carpeta de caché. |
22 | */ |
23 | const CACHE = "pwamd" |
24 | |
25 | /** |
26 | * Archivos requeridos para que la aplicación funcione fuera de |
27 | * línea. |
28 | */ |
29 | const ARCHIVOS = [ |
30 | "ayuda.html", |
31 | "botones.html", |
32 | "campos.html", |
33 | "favicon.ico", |
34 | "formulario.html", |
35 | "iconos.html", |
36 | "index.html", |
37 | "interruptor.html", |
38 | "navbar.html", |
39 | "navtab.html", |
40 | "navTabFixed.html", |
41 | "one-line.html", |
42 | "secundaria.html", |
43 | "segmentado.html", |
44 | "select.html", |
45 | "site.webmanifest", |
46 | "slider.html", |
47 | "tarjetas.html", |
48 | "three-line.html", |
49 | "two-line.html", |
50 | "css/dark-hc.css", |
51 | "css/dark-mc.css", |
52 | "css/dark.css", |
53 | "css/estilos.css", |
54 | "css/light-hc.css", |
55 | "css/light-mc.css", |
56 | "css/light.css", |
57 | "css/transicion_completa.css", |
58 | "css/transicion_pestanas.css", |
59 | "img/Escultura_de_coyote.jpeg", |
60 | "img/icono2048.png", |
61 | "img/maskable_icon.png", |
62 | "img/maskable_icon_x128.png", |
63 | "img/maskable_icon_x192.png", |
64 | "img/maskable_icon_x384.png", |
65 | "img/maskable_icon_x48.png", |
66 | "img/maskable_icon_x512.png", |
67 | "img/maskable_icon_x72.png", |
68 | "img/maskable_icon_x96.png", |
69 | "img/pexels-craig-dennis-3701822.jpg", |
70 | "img/pexels-creative-workshop-3978352.jpg", |
71 | "img/pexels-erik-karits-3732453.jpg", |
72 | "img/pexels-esteban-arango-10226903.jpg", |
73 | "img/pexels-moises-patrício-10961948.jpg", |
74 | "img/pexels-ralph-2270848.jpg", |
75 | "img/pexels-rasmus-svinding-35435.jpg", |
76 | "img/pexels-steve-397857.jpg", |
77 | "img/pexels-vadim-b-141496.jpg", |
78 | "img/screenshot_horizontal.png", |
79 | "img/screenshot_vertical.png", |
80 | "js/configura.js", |
81 | "js/nav-bar.js", |
82 | "js/nav-drw.js", |
83 | "js/nav-tab-fixed.js", |
84 | "js/nav-tab-scrollable.js", |
85 | "js/registraServiceWorker.js", |
86 | "lib/css/material-symbols-outlined.css", |
87 | "lib/css/md-cards.css", |
88 | "lib/css/md-fab-primary.css", |
89 | "lib/css/md-filled-button.css", |
90 | "lib/css/md-filled-text-field.css", |
91 | "lib/css/md-list.css", |
92 | "lib/css/md-menu.css", |
93 | "lib/css/md-navigation-bar.css", |
94 | "lib/css/md-outline-button.css", |
95 | "lib/css/md-ripple.css", |
96 | "lib/css/md-segmented-button.css", |
97 | "lib/css/md-slider-field.css", |
98 | "lib/css/md-standard-icon-button.css", |
99 | "lib/css/md-switch.css", |
100 | "lib/css/md-tab.css", |
101 | "lib/css/md-top-app-bar.css", |
102 | "lib/css/roboto.css", |
103 | "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints", |
104 | "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf", |
105 | "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2", |
106 | "lib/fonts/roboto-v32-latin-regular.woff2", |
107 | "lib/js/abreElementoHtml.js", |
108 | "lib/js/cierraElementoHtmo.js", |
109 | "lib/js/exportaAHtml.js", |
110 | "lib/js/getAttribute.js", |
111 | "lib/js/htmlentities.js", |
112 | "lib/js/muestraError.js", |
113 | "lib/js/muestraTextoDeAyuda.js", |
114 | "lib/js/ProblemDetails.js", |
115 | "lib/js/querySelector.js", |
116 | "lib/js/resaltaSiEstasEn.js", |
117 | "lib/js/const/ES_APPLE.js", |
118 | "lib/js/custom/md-menu-button.js", |
119 | "lib/js/custom/md-options-menu.js", |
120 | "lib/js/custom/md-overflow-button.js", |
121 | "lib/js/custom/md-overflow-menu.js", |
122 | "lib/js/custom/md-select-menu.js", |
123 | "lib/js/custom/md-slider-field.js", |
124 | "lib/js/custom/md-top-app-bar.js", |
125 | "lib/js/custom/MdNavigationDrawer.js", |
126 | "material-tokens/css/baseline.css", |
127 | "material-tokens/css/colors.css", |
128 | "material-tokens/css/elevation.css", |
129 | "material-tokens/css/motion.css", |
130 | "material-tokens/css/palette.css", |
131 | "material-tokens/css/shape.css", |
132 | "material-tokens/css/state.css", |
133 | "material-tokens/css/typography.css", |
134 | "material-tokens/css/theme/dark.css", |
135 | "material-tokens/css/theme/light.css", |
136 | "ungap/custom-elements.js", |
137 | "/" |
138 | ] |
139 | |
140 | // Verifica si el código corre dentro de un service worker. |
141 | if (self instanceof ServiceWorkerGlobalScope) { |
142 | // Evento al empezar a instalar el servide worker, |
143 | self.addEventListener("install", |
144 | (/** @type {ExtendableEvent} */ evt) => { |
145 | console.log("El service worker se está instalando.") |
146 | evt.waitUntil(llenaElCache()) |
147 | }) |
148 | |
149 | // Evento al solicitar información a la red. |
150 | self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => { |
151 | if (evt.request.method === "GET") { |
152 | evt.respondWith(buscaLaRespuestaEnElCache(evt)) |
153 | } |
154 | }) |
155 | |
156 | // Evento cuando el service worker se vuelve activo. |
157 | self.addEventListener("activate", |
158 | () => console.log("El service worker está activo.")) |
159 | } |
160 | |
161 | async function llenaElCache() { |
162 | console.log("Intentando cargar caché:", CACHE) |
163 | // Borra todos los cachés. |
164 | const keys = await caches.keys() |
165 | for (const key of keys) { |
166 | await caches.delete(key) |
167 | } |
168 | // Abre el caché de este service worker. |
169 | const cache = await caches.open(CACHE) |
170 | // Carga el listado de ARCHIVOS. |
171 | await cache.addAll(ARCHIVOS) |
172 | console.log("Cache cargado:", CACHE) |
173 | console.log("Versión:", VERSION) |
174 | } |
175 | |
176 | /** @param {FetchEvent} evt */ |
177 | async function buscaLaRespuestaEnElCache(evt) { |
178 | // Abre el caché. |
179 | const cache = await caches.open(CACHE) |
180 | const request = evt.request |
181 | /* Busca la respuesta a la solicitud en el contenido del caché, sin |
182 | * tomar en cuenta la parte después del símbolo "?" en la URL. */ |
183 | const response = await cache.match(request, { ignoreSearch: true }) |
184 | if (response === undefined) { |
185 | /* Si no la encuentra, empieza a descargar de la red y devuelve |
186 | * la promesa. */ |
187 | return fetch(request) |
188 | } else { |
189 | // Si la encuentra, devuelve la respuesta encontrada en el caché. |
190 | return response |
191 | } |
192 | } |
1 | .dark-high-contrast { |
2 | --md-sys-color-primary: rgb(251 250 255); |
3 | --md-sys-color-surface-tint: rgb(170 199 255); |
4 | --md-sys-color-on-primary: rgb(0 0 0); |
5 | --md-sys-color-primary-container: rgb(177 203 255); |
6 | --md-sys-color-on-primary-container: rgb(0 0 0); |
7 | --md-sys-color-secondary: rgb(251 250 255); |
8 | --md-sys-color-on-secondary: rgb(0 0 0); |
9 | --md-sys-color-secondary-container: rgb(194 203 224); |
10 | --md-sys-color-on-secondary-container: rgb(0 0 0); |
11 | --md-sys-color-tertiary: rgb(255 249 250); |
12 | --md-sys-color-on-tertiary: rgb(0 0 0); |
13 | --md-sys-color-tertiary-container: rgb(225 192 229); |
14 | --md-sys-color-on-tertiary-container: rgb(0 0 0); |
15 | --md-sys-color-error: rgb(255 249 249); |
16 | --md-sys-color-on-error: rgb(0 0 0); |
17 | --md-sys-color-error-container: rgb(255 186 177); |
18 | --md-sys-color-on-error-container: rgb(0 0 0); |
19 | --md-sys-color-background: rgb(17 19 24); |
20 | --md-sys-color-on-background: rgb(226 226 233); |
21 | --md-sys-color-surface: rgb(17 19 24); |
22 | --md-sys-color-on-surface: rgb(255 255 255); |
23 | --md-sys-color-surface-variant: rgb(68 71 78); |
24 | --md-sys-color-on-surface-variant: rgb(251 250 255); |
25 | --md-sys-color-outline: rgb(200 202 212); |
26 | --md-sys-color-outline-variant: rgb(200 202 212); |
27 | --md-sys-color-shadow: rgb(0 0 0); |
28 | --md-sys-color-scrim: rgb(0 0 0); |
29 | --md-sys-color-inverse-surface: rgb(226 226 233); |
30 | --md-sys-color-inverse-on-surface: rgb(0 0 0); |
31 | --md-sys-color-inverse-primary: rgb(0 41 89); |
32 | --md-sys-color-primary-fixed: rgb(221 231 255); |
33 | --md-sys-color-on-primary-fixed: rgb(0 0 0); |
34 | --md-sys-color-primary-fixed-dim: rgb(177 203 255); |
35 | --md-sys-color-on-primary-fixed-variant: rgb(0 22 52); |
36 | --md-sys-color-secondary-fixed: rgb(222 231 253); |
37 | --md-sys-color-on-secondary-fixed: rgb(0 0 0); |
38 | --md-sys-color-secondary-fixed-dim: rgb(194 203 224); |
39 | --md-sys-color-on-secondary-fixed-variant: rgb(13 22 38); |
40 | --md-sys-color-tertiary-fixed: rgb(252 221 255); |
41 | --md-sys-color-on-tertiary-fixed: rgb(0 0 0); |
42 | --md-sys-color-tertiary-fixed-dim: rgb(225 192 229); |
43 | --md-sys-color-on-tertiary-fixed-variant: rgb(35 14 41); |
44 | --md-sys-color-surface-dim: rgb(17 19 24); |
45 | --md-sys-color-surface-bright: rgb(55 57 62); |
46 | --md-sys-color-surface-container-lowest: rgb(12 14 19); |
47 | --md-sys-color-surface-container-low: rgb(25 28 32); |
48 | --md-sys-color-surface-container: rgb(29 32 36); |
49 | --md-sys-color-surface-container-high: rgb(40 42 47); |
50 | --md-sys-color-surface-container-highest: rgb(51 53 58); |
51 | } |
52 |
1 | .dark-medium-contrast { |
2 | --md-sys-color-primary: rgb(177 203 255); |
3 | --md-sys-color-surface-tint: rgb(170 199 255); |
4 | --md-sys-color-on-primary: rgb(0 22 52); |
5 | --md-sys-color-primary-container: rgb(116 145 199); |
6 | --md-sys-color-on-primary-container: rgb(0 0 0); |
7 | --md-sys-color-secondary: rgb(194 203 224); |
8 | --md-sys-color-on-secondary: rgb(13 22 38); |
9 | --md-sys-color-secondary-container: rgb(136 145 165); |
10 | --md-sys-color-on-secondary-container: rgb(0 0 0); |
11 | --md-sys-color-tertiary: rgb(225 192 229); |
12 | --md-sys-color-on-tertiary: rgb(35 14 41); |
13 | --md-sys-color-tertiary-container: rgb(164 135 169); |
14 | --md-sys-color-on-tertiary-container: rgb(0 0 0); |
15 | --md-sys-color-error: rgb(255 186 177); |
16 | --md-sys-color-on-error: rgb(55 0 1); |
17 | --md-sys-color-error-container: rgb(255 84 73); |
18 | --md-sys-color-on-error-container: rgb(0 0 0); |
19 | --md-sys-color-background: rgb(17 19 24); |
20 | --md-sys-color-on-background: rgb(226 226 233); |
21 | --md-sys-color-surface: rgb(17 19 24); |
22 | --md-sys-color-on-surface: rgb(251 250 255); |
23 | --md-sys-color-surface-variant: rgb(68 71 78); |
24 | --md-sys-color-on-surface-variant: rgb(200 202 212); |
25 | --md-sys-color-outline: rgb(160 163 172); |
26 | --md-sys-color-outline-variant: rgb(128 131 140); |
27 | --md-sys-color-shadow: rgb(0 0 0); |
28 | --md-sys-color-scrim: rgb(0 0 0); |
29 | --md-sys-color-inverse-surface: rgb(226 226 233); |
30 | --md-sys-color-inverse-on-surface: rgb(40 42 47); |
31 | --md-sys-color-inverse-primary: rgb(41 72 120); |
32 | --md-sys-color-primary-fixed: rgb(214 227 255); |
33 | --md-sys-color-on-primary-fixed: rgb(0 17 43); |
34 | --md-sys-color-primary-fixed-dim: rgb(170 199 255); |
35 | --md-sys-color-on-primary-fixed-variant: rgb(19 54 101); |
36 | --md-sys-color-secondary-fixed: rgb(218 226 249); |
37 | --md-sys-color-on-secondary-fixed: rgb(8 17 33); |
38 | --md-sys-color-secondary-fixed-dim: rgb(190 198 220); |
39 | --md-sys-color-on-secondary-fixed-variant: rgb(46 54 71); |
40 | --md-sys-color-tertiary-fixed: rgb(250 216 253); |
41 | --md-sys-color-on-tertiary-fixed: rgb(29 8 35); |
42 | --md-sys-color-tertiary-fixed-dim: rgb(221 188 224); |
43 | --md-sys-color-on-tertiary-fixed-variant: rgb(69 46 74); |
44 | --md-sys-color-surface-dim: rgb(17 19 24); |
45 | --md-sys-color-surface-bright: rgb(55 57 62); |
46 | --md-sys-color-surface-container-lowest: rgb(12 14 19); |
47 | --md-sys-color-surface-container-low: rgb(25 28 32); |
48 | --md-sys-color-surface-container: rgb(29 32 36); |
49 | --md-sys-color-surface-container-high: rgb(40 42 47); |
50 | --md-sys-color-surface-container-highest: rgb(51 53 58); |
51 | } |
52 |
1 | .dark { |
2 | --md-sys-color-primary: rgb(170 199 255); |
3 | --md-sys-color-surface-tint: rgb(170 199 255); |
4 | --md-sys-color-on-primary: rgb(10 48 95); |
5 | --md-sys-color-primary-container: rgb(40 71 119); |
6 | --md-sys-color-on-primary-container: rgb(214 227 255); |
7 | --md-sys-color-secondary: rgb(190 198 220); |
8 | --md-sys-color-on-secondary: rgb(40 49 65); |
9 | --md-sys-color-secondary-container: rgb(62 71 89); |
10 | --md-sys-color-on-secondary-container: rgb(218 226 249); |
11 | --md-sys-color-tertiary: rgb(221 188 224); |
12 | --md-sys-color-on-tertiary: rgb(63 40 68); |
13 | --md-sys-color-tertiary-container: rgb(87 62 92); |
14 | --md-sys-color-on-tertiary-container: rgb(250 216 253); |
15 | --md-sys-color-error: rgb(255 180 171); |
16 | --md-sys-color-on-error: rgb(105 0 5); |
17 | --md-sys-color-error-container: rgb(147 0 10); |
18 | --md-sys-color-on-error-container: rgb(255 218 214); |
19 | --md-sys-color-background: rgb(17 19 24); |
20 | --md-sys-color-on-background: rgb(226 226 233); |
21 | --md-sys-color-surface: rgb(17 19 24); |
22 | --md-sys-color-on-surface: rgb(226 226 233); |
23 | --md-sys-color-surface-variant: rgb(68 71 78); |
24 | --md-sys-color-on-surface-variant: rgb(196 198 208); |
25 | --md-sys-color-outline: rgb(142 144 153); |
26 | --md-sys-color-outline-variant: rgb(68 71 78); |
27 | --md-sys-color-shadow: rgb(0 0 0); |
28 | --md-sys-color-scrim: rgb(0 0 0); |
29 | --md-sys-color-inverse-surface: rgb(226 226 233); |
30 | --md-sys-color-inverse-on-surface: rgb(46 48 54); |
31 | --md-sys-color-inverse-primary: rgb(65 95 145); |
32 | --md-sys-color-primary-fixed: rgb(214 227 255); |
33 | --md-sys-color-on-primary-fixed: rgb(0 27 62); |
34 | --md-sys-color-primary-fixed-dim: rgb(170 199 255); |
35 | --md-sys-color-on-primary-fixed-variant: rgb(40 71 119); |
36 | --md-sys-color-secondary-fixed: rgb(218 226 249); |
37 | --md-sys-color-on-secondary-fixed: rgb(19 28 43); |
38 | --md-sys-color-secondary-fixed-dim: rgb(190 198 220); |
39 | --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89); |
40 | --md-sys-color-tertiary-fixed: rgb(250 216 253); |
41 | --md-sys-color-on-tertiary-fixed: rgb(40 19 46); |
42 | --md-sys-color-tertiary-fixed-dim: rgb(221 188 224); |
43 | --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92); |
44 | --md-sys-color-surface-dim: rgb(17 19 24); |
45 | --md-sys-color-surface-bright: rgb(55 57 62); |
46 | --md-sys-color-surface-container-lowest: rgb(12 14 19); |
47 | --md-sys-color-surface-container-low: rgb(25 28 32); |
48 | --md-sys-color-surface-container: rgb(29 32 36); |
49 | --md-sys-color-surface-container-high: rgb(40 42 47); |
50 | --md-sys-color-surface-container-highest: rgb(51 53 58); |
51 | } |
52 |
1 | /* Selecciona el tema claro para Material Design 3. Puedes elegir |
2 | * light.css, light-mc.css o light-hc.css */ |
3 | @import url(light.css) screen and (prefers-color-scheme: light); |
4 | /* Selecciona el tema oscuro para Material Design 3. Puedes elegir |
5 | * dark.css, dark-mc.css o dark-hc.css */ |
6 | @import url(dark.css) screen and (prefers-color-scheme: dark); |
7 | /* Definiciones para Material Design 3 */ |
8 | @import url(../material-tokens/css/baseline.css); |
9 | /* Fonts utilizados */ |
10 | @import url(../lib/css/roboto.css); |
11 | @import url(../lib/css/material-symbols-outlined.css); |
12 | /* CSS de elementos utilizados */ |
13 | @import url(../lib/css/md-ripple.css); |
14 | @import url(../lib/css/md-top-app-bar.css); |
15 | @import url(../lib/css/md-menu.css); |
16 | @import url(../lib/css/md-standard-icon-button.css); |
17 | @import url(../lib/css/md-fab-primary.css); |
18 | @import url(../lib/css/md-filled-button.css); |
19 | @import url(../lib/css/md-filled-text-field.css); |
20 | @import url(../lib/css/md-outline-button.css); |
21 | @import url(../lib/css/md-switch.css); |
22 | @import url(../lib/css/md-slider-field.css); |
23 | @import url(../lib/css/md-segmented-button.css); |
24 | @import url(../lib/css/md-list.css); |
25 | @import url(../lib/css/md-cards.css); |
26 | @import url(../lib/css/md-tab.css); |
27 | @import url(../lib/css/md-navigation-bar.css); |
28 | |
29 | html { |
30 | /* Indica los temas del sistema operativo que son soportados. */ |
31 | color-scheme: light dark; |
32 | --tabWidth: 3.75rem; |
33 | --anchoNav: 22.5rem; |
34 | } |
35 | |
36 | main { |
37 | max-width: 600px; |
38 | margin-left: auto; |
39 | margin-right: auto; |
40 | } |
41 | |
42 | /* Quita un borde rojo que coloca Firefox. */ |
43 | :-moz-ui-invalid { |
44 | box-shadow: none; |
45 | } |
46 | |
47 | body { |
48 | margin: 0; |
49 | font-family: var(--md-sys-typescale-body-large-font); |
50 | font-weight: var(--md-sys-typescale-body-large-weight); |
51 | font-size: var(--md-sys-typescale-body-large-size); |
52 | font-style: var(--md-sys-typescale-body-large-font-style); |
53 | letter-spacing: var(--md-sys-typescale-body-large-tracking); |
54 | line-height: var(--md-sys-typescale-body-large-line-height); |
55 | text-transform: var(--md-sys-typescale-body-large-text-transform); |
56 | text-decoration: var(--md-sys-typescale-body-large-text-decoration); |
57 | color: var(--md-sys-color-on-background); |
58 | background-color: var(--md-sys-color-background); |
59 | /* Las siguientes líneas Evita los cambios de apariencia al cargar estilos y |
60 | + custom elements, que son conocidos como Flash Of Unstyled Content (fouc). */ |
61 | opacity: 0; |
62 | animation-name: fouc; |
63 | animation-fill-mode: forwards; |
64 | animation-duration: 1.5s; |
65 | } |
66 | |
67 | @keyframes fouc { |
68 | to { |
69 | opacity: 1; |
70 | } |
71 | } |
72 | |
73 | html { |
74 | --Font: -apple-system, BlinkMacSystemFont, roboto, sans-serif; |
75 | --colIntIos: white; |
76 | --colIntIosOnBk: #2acc2a; |
77 | --colIntIosOnBkFc: #1bbb1b; |
78 | --colIntIosOffBk: #dbdbdb; |
79 | --colIntIosOffBkFc: #BDBDBD; |
80 | /* Plain typeface */ |
81 | --md-ref-typeface-plain: var(--Font); |
82 | /* Brand typeface */ |
83 | --md-ref-typeface-brand: var(--Font); |
84 | --md-sys-typescale-label-large-weight-prominent: |
85 | var(--md-ref-typeface-weight-bold); |
86 | --md-box_shadow_level4: |
87 | 0 var(--md-sys-elevation-level4) var(--md-sys-elevation-level4) var(--md-sys-color-shadow); |
88 | --md-box_shadow_level3: |
89 | 0 var(--md-sys-elevation-level3) var(--md-sys-elevation-level3) var(--md-sys-color-shadow); |
90 | --md-box_shadow_level2: |
91 | 0 var(--md-sys-elevation-level2) var(--md-sys-elevation-level2) var(--md-sys-color-shadow); |
92 | --md-box_shadow_level1: |
93 | 0 var(--md-sys-elevation-level1) var(--md-sys-elevation-level1) var(--md-sys-color-shadow); |
94 | --md-box_shadow_level0: none; |
95 | --iconSize: 1.5rem; |
96 | --avatarSize: 2.5rem; |
97 | --imageSize: 3.5rem; |
98 | --videoWidth: 7.125rem; |
99 | --videoHeight: 4rem; |
100 | --scroll-headline-duracion: 2s; |
101 | --md-sys-state-focus-indicator-outer-offset: 0.125rem; |
102 | --md-sys-state-focus-indicator-thickness: 0.1875rem; |
103 | /* Pressed state layer opacity */ |
104 | --state-pressed-transparency-percentage: 84%; |
105 | /* Focus state layer opacity */ |
106 | --state-focus-transparency-percentage: 88%; |
107 | /* Hover state layer opacity */ |
108 | --state-hover-transparency-percentage: 92%; |
109 | } |
110 | |
111 | p { |
112 | margin: 1rem; |
113 | } |
114 | |
115 | a { |
116 | color: var(--md-sys-color-on-background); |
117 | } |
118 | |
119 | @media (prefers-color-scheme: light) { |
120 | html { |
121 | --md-riple-color: #00000020; |
122 | } |
123 | } |
124 | |
125 | @media (prefers-color-scheme: dark) { |
126 | html { |
127 | --md-riple-color: #ffffff40; |
128 | } |
129 | } |
130 | |
131 | @keyframes salePorLaIzquierda { |
132 | to { |
133 | translate: -100vw 0; |
134 | } |
135 | } |
136 | |
137 | @keyframes entraPorLaDerecha { |
138 | from { |
139 | translate: 100vw 0; |
140 | } |
141 | } |
142 | |
143 | @keyframes aparece { |
144 | from { |
145 | opacity: 0; |
146 | } |
147 | } |
148 | |
149 | @keyframes desvanece { |
150 | to { |
151 | opacity: 0; |
152 | } |
153 | } |
1 | .light-high-contrast { |
2 | --md-sys-color-primary: rgb(0 33 74); |
3 | --md-sys-color-surface-tint: rgb(65 95 145); |
4 | --md-sys-color-on-primary: rgb(255 255 255); |
5 | --md-sys-color-primary-container: rgb(35 67 115); |
6 | --md-sys-color-on-primary-container: rgb(255 255 255); |
7 | --md-sys-color-secondary: rgb(25 34 50); |
8 | --md-sys-color-on-secondary: rgb(255 255 255); |
9 | --md-sys-color-secondary-container: rgb(58 67 84); |
10 | --md-sys-color-on-secondary-container: rgb(255 255 255); |
11 | --md-sys-color-tertiary: rgb(48 26 53); |
12 | --md-sys-color-on-tertiary: rgb(255 255 255); |
13 | --md-sys-color-tertiary-container: rgb(82 58 88); |
14 | --md-sys-color-on-tertiary-container: rgb(255 255 255); |
15 | --md-sys-color-error: rgb(78 0 2); |
16 | --md-sys-color-on-error: rgb(255 255 255); |
17 | --md-sys-color-error-container: rgb(140 0 9); |
18 | --md-sys-color-on-error-container: rgb(255 255 255); |
19 | --md-sys-color-background: rgb(249 249 255); |
20 | --md-sys-color-on-background: rgb(25 28 32); |
21 | --md-sys-color-surface: rgb(249 249 255); |
22 | --md-sys-color-on-surface: rgb(0 0 0); |
23 | --md-sys-color-surface-variant: rgb(224 226 236); |
24 | --md-sys-color-on-surface-variant: rgb(33 36 43); |
25 | --md-sys-color-outline: rgb(64 67 74); |
26 | --md-sys-color-outline-variant: rgb(64 67 74); |
27 | --md-sys-color-shadow: rgb(0 0 0); |
28 | --md-sys-color-scrim: rgb(0 0 0); |
29 | --md-sys-color-inverse-surface: rgb(46 48 54); |
30 | --md-sys-color-inverse-on-surface: rgb(255 255 255); |
31 | --md-sys-color-inverse-primary: rgb(229 236 255); |
32 | --md-sys-color-primary-fixed: rgb(35 67 115); |
33 | --md-sys-color-on-primary-fixed: rgb(255 255 255); |
34 | --md-sys-color-primary-fixed-dim: rgb(4 44 91); |
35 | --md-sys-color-on-primary-fixed-variant: rgb(255 255 255); |
36 | --md-sys-color-secondary-fixed: rgb(58 67 84); |
37 | --md-sys-color-on-secondary-fixed: rgb(255 255 255); |
38 | --md-sys-color-secondary-fixed-dim: rgb(36 45 61); |
39 | --md-sys-color-on-secondary-fixed-variant: rgb(255 255 255); |
40 | --md-sys-color-tertiary-fixed: rgb(82 58 88); |
41 | --md-sys-color-on-tertiary-fixed: rgb(255 255 255); |
42 | --md-sys-color-tertiary-fixed-dim: rgb(59 36 64); |
43 | --md-sys-color-on-tertiary-fixed-variant: rgb(255 255 255); |
44 | --md-sys-color-surface-dim: rgb(217 217 224); |
45 | --md-sys-color-surface-bright: rgb(249 249 255); |
46 | --md-sys-color-surface-container-lowest: rgb(255 255 255); |
47 | --md-sys-color-surface-container-low: rgb(243 243 250); |
48 | --md-sys-color-surface-container: rgb(237 237 244); |
49 | --md-sys-color-surface-container-high: rgb(231 232 238); |
50 | --md-sys-color-surface-container-highest: rgb(226 226 233); |
51 | } |
52 |
1 | .light-medium-contrast { |
2 | --md-sys-color-primary: rgb(35 67 115); |
3 | --md-sys-color-surface-tint: rgb(65 95 145); |
4 | --md-sys-color-on-primary: rgb(255 255 255); |
5 | --md-sys-color-primary-container: rgb(88 117 168); |
6 | --md-sys-color-on-primary-container: rgb(255 255 255); |
7 | --md-sys-color-secondary: rgb(58 67 84); |
8 | --md-sys-color-on-secondary: rgb(255 255 255); |
9 | --md-sys-color-secondary-container: rgb(108 117 136); |
10 | --md-sys-color-on-secondary-container: rgb(255 255 255); |
11 | --md-sys-color-tertiary: rgb(82 58 88); |
12 | --md-sys-color-on-tertiary: rgb(255 255 255); |
13 | --md-sys-color-tertiary-container: rgb(135 107 140); |
14 | --md-sys-color-on-tertiary-container: rgb(255 255 255); |
15 | --md-sys-color-error: rgb(140 0 9); |
16 | --md-sys-color-on-error: rgb(255 255 255); |
17 | --md-sys-color-error-container: rgb(218 52 46); |
18 | --md-sys-color-on-error-container: rgb(255 255 255); |
19 | --md-sys-color-background: rgb(249 249 255); |
20 | --md-sys-color-on-background: rgb(25 28 32); |
21 | --md-sys-color-surface: rgb(249 249 255); |
22 | --md-sys-color-on-surface: rgb(25 28 32); |
23 | --md-sys-color-surface-variant: rgb(224 226 236); |
24 | --md-sys-color-on-surface-variant: rgb(64 67 74); |
25 | --md-sys-color-outline: rgb(92 95 103); |
26 | --md-sys-color-outline-variant: rgb(120 122 131); |
27 | --md-sys-color-shadow: rgb(0 0 0); |
28 | --md-sys-color-scrim: rgb(0 0 0); |
29 | --md-sys-color-inverse-surface: rgb(46 48 54); |
30 | --md-sys-color-inverse-on-surface: rgb(240 240 247); |
31 | --md-sys-color-inverse-primary: rgb(170 199 255); |
32 | --md-sys-color-primary-fixed: rgb(88 117 168); |
33 | --md-sys-color-on-primary-fixed: rgb(255 255 255); |
34 | --md-sys-color-primary-fixed-dim: rgb(62 92 142); |
35 | --md-sys-color-on-primary-fixed-variant: rgb(255 255 255); |
36 | --md-sys-color-secondary-fixed: rgb(108 117 136); |
37 | --md-sys-color-on-secondary-fixed: rgb(255 255 255); |
38 | --md-sys-color-secondary-fixed-dim: rgb(83 92 111); |
39 | --md-sys-color-on-secondary-fixed-variant: rgb(255 255 255); |
40 | --md-sys-color-tertiary-fixed: rgb(135 107 140); |
41 | --md-sys-color-on-tertiary-fixed: rgb(255 255 255); |
42 | --md-sys-color-tertiary-fixed-dim: rgb(109 83 114); |
43 | --md-sys-color-on-tertiary-fixed-variant: rgb(255 255 255); |
44 | --md-sys-color-surface-dim: rgb(217 217 224); |
45 | --md-sys-color-surface-bright: rgb(249 249 255); |
46 | --md-sys-color-surface-container-lowest: rgb(255 255 255); |
47 | --md-sys-color-surface-container-low: rgb(243 243 250); |
48 | --md-sys-color-surface-container: rgb(237 237 244); |
49 | --md-sys-color-surface-container-high: rgb(231 232 238); |
50 | --md-sys-color-surface-container-highest: rgb(226 226 233); |
51 | } |
52 |
1 | .light { |
2 | --md-sys-color-primary: rgb(65 95 145); |
3 | --md-sys-color-surface-tint: rgb(65 95 145); |
4 | --md-sys-color-on-primary: rgb(255 255 255); |
5 | --md-sys-color-primary-container: rgb(214 227 255); |
6 | --md-sys-color-on-primary-container: rgb(0 27 62); |
7 | --md-sys-color-secondary: rgb(86 95 113); |
8 | --md-sys-color-on-secondary: rgb(255 255 255); |
9 | --md-sys-color-secondary-container: rgb(218 226 249); |
10 | --md-sys-color-on-secondary-container: rgb(19 28 43); |
11 | --md-sys-color-tertiary: rgb(112 85 117); |
12 | --md-sys-color-on-tertiary: rgb(255 255 255); |
13 | --md-sys-color-tertiary-container: rgb(250 216 253); |
14 | --md-sys-color-on-tertiary-container: rgb(40 19 46); |
15 | --md-sys-color-error: rgb(186 26 26); |
16 | --md-sys-color-on-error: rgb(255 255 255); |
17 | --md-sys-color-error-container: rgb(255 218 214); |
18 | --md-sys-color-on-error-container: rgb(65 0 2); |
19 | --md-sys-color-background: rgb(249 249 255); |
20 | --md-sys-color-on-background: rgb(25 28 32); |
21 | --md-sys-color-surface: rgb(249 249 255); |
22 | --md-sys-color-on-surface: rgb(25 28 32); |
23 | --md-sys-color-surface-variant: rgb(224 226 236); |
24 | --md-sys-color-on-surface-variant: rgb(68 71 78); |
25 | --md-sys-color-outline: rgb(116 119 127); |
26 | --md-sys-color-outline-variant: rgb(196 198 208); |
27 | --md-sys-color-shadow: rgb(0 0 0); |
28 | --md-sys-color-scrim: rgb(0 0 0); |
29 | --md-sys-color-inverse-surface: rgb(46 48 54); |
30 | --md-sys-color-inverse-on-surface: rgb(240 240 247); |
31 | --md-sys-color-inverse-primary: rgb(170 199 255); |
32 | --md-sys-color-primary-fixed: rgb(214 227 255); |
33 | --md-sys-color-on-primary-fixed: rgb(0 27 62); |
34 | --md-sys-color-primary-fixed-dim: rgb(170 199 255); |
35 | --md-sys-color-on-primary-fixed-variant: rgb(40 71 119); |
36 | --md-sys-color-secondary-fixed: rgb(218 226 249); |
37 | --md-sys-color-on-secondary-fixed: rgb(19 28 43); |
38 | --md-sys-color-secondary-fixed-dim: rgb(190 198 220); |
39 | --md-sys-color-on-secondary-fixed-variant: rgb(62 71 89); |
40 | --md-sys-color-tertiary-fixed: rgb(250 216 253); |
41 | --md-sys-color-on-tertiary-fixed: rgb(40 19 46); |
42 | --md-sys-color-tertiary-fixed-dim: rgb(221 188 224); |
43 | --md-sys-color-on-tertiary-fixed-variant: rgb(87 62 92); |
44 | --md-sys-color-surface-dim: rgb(217 217 224); |
45 | --md-sys-color-surface-bright: rgb(249 249 255); |
46 | --md-sys-color-surface-container-lowest: rgb(255 255 255); |
47 | --md-sys-color-surface-container-low: rgb(243 243 250); |
48 | --md-sys-color-surface-container: rgb(237 237 244); |
49 | --md-sys-color-surface-container-high: rgb(231 232 238); |
50 | --md-sys-color-surface-container-highest: rgb(226 226 233); |
51 | } |
52 |
1 | |
2 | @view-transition { |
3 | navigation: auto; |
4 | } |
5 | |
6 | ::view-transition-group(root) { |
7 | animation-duration: var(--md-sys-motion-duration-700); |
8 | } |
9 | |
10 | html::view-transition-old(root) { |
11 | animation-name: desvanece; |
12 | } |
13 | |
14 | html::view-transition-new(root) { |
15 | animation-delay: var(--md-sys-motion-duration-700); |
16 | animation-name: aparece; |
17 | } |
1 | @view-transition { |
2 | navigation: auto; |
3 | } |
4 | |
5 | #headline { |
6 | view-transition-name: encabezado; |
7 | } |
8 | |
9 | main { |
10 | view-transition-name: contenido; |
11 | } |
12 | |
13 | ::view-transition-group(encabezado) { |
14 | animation-duration: var(--md-sys-motion-duration-1000); |
15 | } |
16 | |
17 | ::view-transition-group(contenido) { |
18 | animation-duration: var(--md-sys-motion-duration-1000); |
19 | } |
20 | |
21 | html::view-transition-old(encabezado) { |
22 | animation-name: salePorLaIzquierda; |
23 | } |
24 | |
25 | html::view-transition-new(encabezado) { |
26 | animation-name: entraPorLaDerecha; |
27 | } |
28 | |
29 | html::view-transition-old(contenido) { |
30 | animation-name: salePorLaIzquierda; |
31 | } |
32 | |
33 | html::view-transition-new(contenido) { |
34 | animation-duration: var(--md-sys-motion-duration-700); |
35 | animation-name: entraPorLaDerecha; |
36 | } |
Es el ícono que se muestra en las pestañas del navegador. Normalmente debe ser de 32x32 pixeles y debe colocarse en la carpeta raíz del proyecto.
La foto del Coyote de Neza es de Ludres Isan, publicada en Pinterest. Se puede localizar en https://www.pinterest.com.mx/ludresi/
Foto de Craig Dennis en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/
Foto de Creative Workshop en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/perro-de-pelo-corto-marron-y-blanco-acostado-3978352/
Foto de Erik Karits en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/foto-de-buho-ural-3732453/
Foto de Esteban Arango en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/animal-perro-mono-hierba-10226903/
Foto de Moises Patrício en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/
Foto de Ralph en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/leon-marron-2270848/
Foto de Rasmus Svinding en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/oso-cafe-35435/
Foto de Steve en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/lobo-blanco-y-negro-397857/
Foto de Vadim B en Pexels. Se puede localizar en https://www.pexels.com/es-es/foto/gatito-gris-en-bolsa-de-papel-plateada-141496/
1 | @font-face { |
2 | font-family: 'Material Symbols Outlined'; |
3 | font-style: normal; |
4 | src: |
5 | url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].woff2) format('woff2'), |
6 | url(../fonts/MaterialSymbolsOutlined[FILL\,GRAD\,opsz\,wght].ttf) format('truetype'); |
7 | } |
8 | |
9 | .material-symbols-outlined { |
10 | font-family: 'Material Symbols Outlined'; |
11 | font-weight: normal; |
12 | font-style: normal; |
13 | font-size: 1.5rem; |
14 | width: 1.5rem; |
15 | height: 1.5rem; |
16 | /* Preferred icon size */ |
17 | display: inline-block; |
18 | line-height: 1; |
19 | text-transform: none; |
20 | letter-spacing: normal; |
21 | word-wrap: normal; |
22 | white-space: nowrap; |
23 | direction: ltr; |
24 | } |
1 | .md-cards { |
2 | margin: 0.5rem; |
3 | gap: 0.5rem; |
4 | display: grid; |
5 | grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); |
6 | } |
7 | |
8 | /* container */ |
9 | .md-cards>*::before { |
10 | content: ""; |
11 | position: absolute; |
12 | z-index: -2; |
13 | top: 0; |
14 | right: 0; |
15 | left: 0; |
16 | bottom: 0; |
17 | background-color: var(--md-sys-color-surface-variant); |
18 | } |
19 | |
20 | /* state layer */ |
21 | .md-cards>*::after { |
22 | content: ""; |
23 | position: absolute; |
24 | z-index: -1; |
25 | top: 0; |
26 | right: 0; |
27 | left: 0; |
28 | bottom: 0; |
29 | background-color: transparent; |
30 | } |
31 | |
32 | .md-cards>* { |
33 | position: relative; |
34 | display: block; |
35 | text-decoration: none; |
36 | color: var(--md-sys-color-on-surface-variant); |
37 | border-radius: 0.75rem; |
38 | overflow: hidden; |
39 | box-shadow: var(--md-box_shadow_level0); |
40 | } |
41 | |
42 | /* state layer */ |
43 | .md-cards>:hover::after { |
44 | background-color: var(--md-sys-color-on-surface-variant); |
45 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
46 | } |
47 | |
48 | .md-cards>a:hover { |
49 | box-shadow: var(--md-box_shadow_level1); |
50 | } |
51 | |
52 | /* state layer */ |
53 | .md-cards>:focus::after { |
54 | background-color: var(--md-sys-color-on-surface-variant); |
55 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
56 | } |
57 | |
58 | .md-cards>a:focus { |
59 | outline: none; |
60 | } |
61 | |
62 | /* state layer */ |
63 | .md-cards>:active::after { |
64 | background-color: var(--md-sys-color-on-surface-variant); |
65 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
66 | } |
67 | |
68 | .md-cards a:active { |
69 | background-position: center; |
70 | background-image: |
71 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
72 | background-size: 100%; |
73 | animation-name: md-ripple; |
74 | animation-duration: var(--md-sys-motion-duration-500); |
75 | box-shadow: var(--md-box_shadow_level0) !important; |
76 | } |
77 | |
78 | .md-cards>*>* { |
79 | display: block; |
80 | } |
81 | |
82 | .md-cards figure { |
83 | border-radius: 0.75rem; |
84 | padding: 0; |
85 | margin: 0; |
86 | width: 100%; |
87 | } |
88 | |
89 | .md-cards figure * { |
90 | border-radius: 0.75rem; |
91 | width: 100%; |
92 | } |
93 | |
94 | .md-cards .headline { |
95 | margin: 1rem; |
96 | font-family: var(--md-sys-typescale-headline-small-font); |
97 | font-weight: var(--md-sys-typescale-headline-small-weight); |
98 | font-size: var(--md-sys-typescale-headline-small-size); |
99 | font-style: var(--md-sys-typescale-headline-small-font-style); |
100 | letter-spacing: var(--md-sys-typescale-headline-small-tracking); |
101 | line-height: var(--md-sys-typescale-headline-small-line-height); |
102 | text-transform: var(--md-sys-typescale-headline-small-text-transform); |
103 | text-decoration: var(--md-sys-typescale-headline-small-text-decoration); |
104 | } |
105 | |
106 | .md-cards a .headline { |
107 | text-decoration: underline; |
108 | } |
109 | |
110 | .md-cards .supporting { |
111 | margin: 1rem; |
112 | font-family: var(--md-sys-typescale-body-large-font); |
113 | font-weight: var(--md-sys-typescale-body-large-weight); |
114 | font-size: var(--md-sys-typescale-body-large-size); |
115 | font-style: var(--md-sys-typescale-body-large-font-style); |
116 | letter-spacing: var(--md-sys-typescale-body-large-tracking); |
117 | line-height: var(--md-sys-typescale-body-large-line-height); |
118 | text-transform: var(--md-sys-typescale-body-large-text-transform); |
119 | text-decoration: var(--md-sys-typescale-body-large-text-decoration); |
120 | } |
1 | .md-fab-primary { |
2 | position: relative; |
3 | display: inline-block; |
4 | border: none; |
5 | width: 3.5rem; |
6 | height: 3.5rem; |
7 | border-radius: var(--md-sys-shape-corner-large-default-size); |
8 | overflow: hidden; |
9 | padding: 0; |
10 | padding-block: 0; |
11 | padding-inline: 0; |
12 | text-decoration: none; |
13 | background-color: var(--md-sys-color-primary-container); |
14 | box-shadow: var(--md-box_shadow_level3); |
15 | } |
16 | |
17 | .md-fab-primary[hidden] { |
18 | display: none; |
19 | } |
20 | |
21 | /* state layer */ |
22 | .md-fab-primary::after { |
23 | content: ""; |
24 | position: absolute; |
25 | top: 0; |
26 | right: 0; |
27 | left: 0; |
28 | bottom: 0; |
29 | } |
30 | |
31 | .md-fab-primary span { |
32 | position: relative; |
33 | color: var(--md-sys-color-on-primary-container); |
34 | } |
35 | |
36 | .md-fab-primary:hover { |
37 | box-shadow: var(--md-box_shadow_level4); |
38 | } |
39 | |
40 | .md-fab-primary:hover::after { |
41 | background-color: var(--md-sys-color-on-primary-container); |
42 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
43 | } |
44 | |
45 | .md-fab-primary:hover span { |
46 | color: var(--md-sys-color-on-primary-container); |
47 | } |
48 | |
49 | |
50 | .md-fab-primary:focus { |
51 | box-shadow: var(--md-box_shadow_level3); |
52 | outline:none; |
53 | /* outline: |
54 | var(--md-sys-color-secondary) var(--md-sys-state-focus-indicator-thickness); |
55 | outline-offset: var(--md-sys-state-focus-indicator-outer-offset); */ |
56 | } |
57 | |
58 | .md-fab-primary:focus::after { |
59 | background-color: var(--md-sys-color-on-primary-container); |
60 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
61 | } |
62 | |
63 | .md-fab-primary:focus span { |
64 | color: var(--md-sys-color-on-primary-container); |
65 | } |
66 | |
67 | .md-fab-primary:active { |
68 | box-shadow: var(--md-box_shadow_level3); |
69 | background-position: center; |
70 | background-image: |
71 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
72 | background-size: 100%; |
73 | animation-name: md-ripple; |
74 | animation-duration: var(--md-sys-motion-duration-500); |
75 | } |
76 | |
77 | .md-fab-primary:active::after { |
78 | background-color: var(--md-sys-color-on-primary-container); |
79 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
80 | } |
81 | |
82 | .md-fab-primary:active span { |
83 | color: var(--md-sys-color-on-primary-container); |
84 | } |
1 | /* container */ |
2 | .md-filled-button::before { |
3 | content: ""; |
4 | position: absolute; |
5 | z-index: -2; |
6 | top: 0; |
7 | right: 0; |
8 | left: 0; |
9 | bottom: 0; |
10 | background-color: var(--md-sys-color-primary); |
11 | } |
12 | |
13 | /* state layer */ |
14 | .md-filled-button::after { |
15 | content: ""; |
16 | position: absolute; |
17 | z-index: -1; |
18 | top: 0; |
19 | right: 0; |
20 | left: 0; |
21 | bottom: 0; |
22 | background-color: transparent; |
23 | } |
24 | |
25 | /* label, shape */ |
26 | .md-filled-button { |
27 | position: relative; |
28 | box-sizing: border-box; |
29 | border-radius: 1.25rem; |
30 | height: 2.5rem; |
31 | line-height: 2.5rem; |
32 | padding: 0 1.5rem; |
33 | border: none; |
34 | background-color: transparent; |
35 | box-shadow: var(--md-box_shadow_level0); |
36 | font-family: var(--md-sys-typescale-label-large-font); |
37 | font-weight: var(--md-sys-typescale-label-large-weight); |
38 | font-size: var(--md-sys-typescale-label-large-size); |
39 | font-style: var(--md-sys-typescale-label-large-font-style); |
40 | letter-spacing: var(--md-sys-typescale-label-large-tracking); |
41 | text-transform: var(--md-sys-typescale-label-large-text-transform); |
42 | text-decoration: var(--md-sys-typescale-label-large-text-decoration); |
43 | color: var(--md-sys-color-on-primary); |
44 | white-space: nowrap; |
45 | text-overflow: ellipsis; |
46 | overflow: hidden; |
47 | } |
48 | |
49 | /* label, shape */ |
50 | .md-filled-button:hover { |
51 | color: var(--md-sys-color-on-primary); |
52 | box-shadow: var(--md-box_shadow_level1); |
53 | } |
54 | |
55 | /* state layer */ |
56 | .md-filled-button:hover::after { |
57 | background-color: var(--md-sys-color-on-primary); |
58 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
59 | } |
60 | |
61 | /* label, shape */ |
62 | .md-filled-button:focus { |
63 | outline: none; |
64 | color: var(--md-sys-color-on-primary); |
65 | box-shadow: var(--md-box_shadow_level0) !important; |
66 | } |
67 | |
68 | /* state layer */ |
69 | .md-filled-button:focus::after { |
70 | background-color: var(--md-sys-color-on-primary); |
71 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
72 | } |
73 | |
74 | /* label, shape */ |
75 | .md-filled-button:active { |
76 | color: var(--md-sys-color-on-primary); |
77 | background-position: center; |
78 | background-image: |
79 | radial-gradient(circle, var(--md-sys-color-on-primary-container) 1%, transparent 1%); |
80 | background-size: 100%; |
81 | animation-name: md-ripple; |
82 | animation-duration: var(--md-sys-motion-duration-500); |
83 | box-shadow: var(--md-box_shadow_level0) !important; |
84 | } |
85 | |
86 | /* state layer */ |
87 | .md-filled-button:active::after { |
88 | background-color: var(--md-sys-color-on-primary); |
89 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
90 | } |
91 | |
92 | /* label, shape */ |
93 | .md-filled-button:disabled { |
94 | background-color: transparent !important; |
95 | color: var(--md-sys-color-on-surface) !important; |
96 | opacity: 0.38 !important; |
97 | box-shadow: var(--md-box_shadow_level0) !important; |
98 | } |
99 | |
100 | /* container */ |
101 | .md-filled-button:disabled::before { |
102 | background-color: var(--md-sys-color-on-surface) !important; |
103 | opacity: 0.12 !important; |
104 | } |
105 | |
106 | /* state layer */ |
107 | .md-filled-button:disabled::after { |
108 | background-color: transparent !important; |
109 | opacity: 1 !important; |
110 | } |
1 | .md-filled-text-field { |
2 | position: relative; |
3 | overflow: hidden; |
4 | display: flex; |
5 | flex-direction: column; |
6 | align-items: stretch; |
7 | padding-top: calc(0.5rem + var(--md-sys-typescale-body-small-line-height)); |
8 | border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left); |
9 | border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right); |
10 | overflow: hidden; |
11 | } |
12 | |
13 | /* container */ |
14 | .md-filled-text-field::before { |
15 | content: ""; |
16 | position: absolute; |
17 | z-index: -2; |
18 | top: 0; |
19 | right: 0; |
20 | left: 0; |
21 | bottom: 0; |
22 | background-color: var(--md-sys-color-surface-container-highest); |
23 | } |
24 | |
25 | /* state layer */ |
26 | .md-filled-text-field::after { |
27 | content: ""; |
28 | position: absolute; |
29 | z-index: -1; |
30 | top: 0; |
31 | right: 0; |
32 | left: 0; |
33 | bottom: 0; |
34 | background-color: transparent; |
35 | } |
36 | |
37 | .md-filled-text-field span, |
38 | .md-filled-text-field label { |
39 | position: absolute; |
40 | top: 0.5rem; |
41 | left: 1rem; |
42 | right: 1rem; |
43 | display: block; |
44 | transform: translateY(1rem); |
45 | transition-property: transform; |
46 | transition-duration: var(--md-sys-motion-duration-300); |
47 | white-space: nowrap; |
48 | text-overflow: ellipsis; |
49 | overflow: hidden; |
50 | color: var(--md-sys-color-on-surface-variant); |
51 | font-family: var(--md-sys-typescale-body-large-font); |
52 | font-weight: var(--md-sys-typescale-body-large-weight); |
53 | font-size: var(--md-sys-typescale-body-large-size); |
54 | font-style: var(--md-sys-typescale-body-large-font-style); |
55 | letter-spacing: var(--md-sys-typescale-body-large-tracking); |
56 | line-height: var(--md-sys-typescale-body-large-line-height); |
57 | text-transform: var(--md-sys-typescale-body-large-text-transform); |
58 | text-decoration: var(--md-sys-typescale-body-large-text-decoration); |
59 | } |
60 | |
61 | .md-filled-text-field :not(:placeholder-shown)+span, |
62 | .md-filled-text-field :not(:placeholder-shown)+label, |
63 | .md-filled-text-field .populated+span, |
64 | .md-filled-text-field .populated+label, |
65 | .md-filled-text-field:focus-within span, |
66 | .md-filled-text-field:focus-within label, |
67 | .md-filled-text-field.float span, |
68 | .md-filled-text-field.float label { |
69 | transform: translateY(0); |
70 | font-family: var(--md-sys-typescale-body-small-font); |
71 | font-weight: var(--md-sys-typescale-body-small-weight); |
72 | font-size: var(--md-sys-typescale-body-small-size); |
73 | font-style: var(--md-sys-typescale-body-small-font-style); |
74 | letter-spacing: var(--md-sys-typescale-body-small-tracking); |
75 | line-height: var(--md-sys-typescale-body-small-line-height); |
76 | text-transform: var(--md-sys-typescale-body-small-text-transform); |
77 | text-decoration: var(--md-sys-typescale-body-small-text-decoration); |
78 | } |
79 | |
80 | .md-filled-text-field :not(label, span, small) { |
81 | position: relative; |
82 | caret-color: var(--md-sys-color-primary); |
83 | min-height: 2rem; |
84 | box-sizing: border-box; |
85 | padding-left: 1rem; |
86 | padding-bottom: 0.5rem; |
87 | padding-right: 1rem; |
88 | border: none; |
89 | resize: none; |
90 | color: var(--md-sys-color-on-surface); |
91 | font-family: var(--md-sys-typescale-body-large-font); |
92 | font-weight: var(--md-sys-typescale-body-large-weight); |
93 | font-size: var(--md-sys-typescale-body-large-size); |
94 | font-style: var(--md-sys-typescale-body-large-font-style); |
95 | letter-spacing: var(--md-sys-typescale-body-large-tracking); |
96 | line-height: var(--md-sys-typescale-body-large-line-height); |
97 | text-transform: var(--md-sys-typescale-body-large-text-transform); |
98 | text-decoration: var(--md-sys-typescale-body-large-text-decoration); |
99 | background-color: transparent; |
100 | outline: none; |
101 | border-bottom-width: 0.0625rem; |
102 | border-bottom-style: solid; |
103 | border-bottom-color: var(--md-sys-color-on-surface-variant); |
104 | } |
105 | |
106 | .md-filled-text-field ::placeholder { |
107 | color: transparent; |
108 | } |
109 | |
110 | .md-filled-text-field small { |
111 | display: block; |
112 | color: var(--md-sys-color-on-surface-variant); |
113 | background-color: var(--md-sys-color-background); |
114 | font-family: var(--md-sys-typescale-body-small-font); |
115 | font-weight: var(--md-sys-typescale-body-small-weight); |
116 | font-size: var(--md-sys-typescale-body-small-size); |
117 | font-style: var(--md-sys-typescale-body-small-font-style); |
118 | letter-spacing: var(--md-sys-typescale-body-small-tracking); |
119 | line-height: var(--md-sys-typescale-body-small-line-height); |
120 | text-transform: var(--md-sys-typescale-body-small-text-transform); |
121 | text-decoration: var(--md-sys-typescale-body-small-text-decoration); |
122 | padding: 0.25rem 1rem 0 1rem; |
123 | white-space: nowrap; |
124 | text-overflow: ellipsis; |
125 | overflow: hidden; |
126 | } |
127 | |
128 | .md-filled-text-field:hover span, |
129 | .md-filled-text-field:hover label { |
130 | color: var(--md-sys-color-on-surface-variant); |
131 | } |
132 | |
133 | .md-filled-text-field:hover :not(label, span, small) { |
134 | padding-bottom: 0.5rem; |
135 | border-bottom-width: 0.0625rem; |
136 | border-bottom-color: var(--md-sys-color-on-surface); |
137 | } |
138 | |
139 | .md-filled-text-field:hover::after { |
140 | background-color: var(--md-sys-color-on-surface); |
141 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
142 | } |
143 | |
144 | .md-filled-text-field:hover small { |
145 | color: var(--md-sys-color-on-surface-variant); |
146 | } |
147 | |
148 | .md-filled-text-field:focus-within span, |
149 | .md-filled-text-field:focus-within label { |
150 | color: var(--md-sys-color-primary); |
151 | } |
152 | |
153 | .md-filled-text-field :focus { |
154 | color: var(--md-sys-color-on-surface); |
155 | outline: none; |
156 | padding-bottom: 0.4375rem; |
157 | border-bottom-width: 0.125rem; |
158 | border-bottom-color: var(--md-sys-color-primary); |
159 | } |
160 | |
161 | .md-filled-text-field:hover :focus { |
162 | padding-bottom: 0.4375rem; |
163 | border-bottom-width: 0.125rem; |
164 | } |
165 | |
166 | .md-filled-text-field:focus-within small { |
167 | color: var(--md-sys-color-on-surface-variant); |
168 | } |
169 | |
170 | .md-filled-text-field :invalid { |
171 | color: var(--md-sys-color-on-surface); |
172 | border-bottom-color: var(--md-sys-color-error); |
173 | } |
174 | |
175 | .md-filled-text-field :invalid+span, |
176 | .md-filled-text-field :invalid+label { |
177 | color: var(--md-sys-color-error); |
178 | } |
179 | |
180 | .md-filled-text-field :invalid~small, |
181 | .md-filled-text-field:hover :invalid~small, |
182 | .md-filled-text-field:focus-within .input-text:invalid~small { |
183 | color: var(--md-sys-color-error); |
184 | } |
185 | |
186 | .md-filled-text-field :invalid:focus { |
187 | caret-color: var(--md-sys-color-error); |
188 | border-bottom-color: var(--md-sys-color-error); |
189 | } |
190 | |
191 | .md-filled-text-field:hover :invalid { |
192 | color: var(--md-sys-color-on-surface); |
193 | border-bottom-color: var(--md-sys-color-error); |
194 | } |
1 | .md-list { |
2 | margin: 0.5rem 0; |
3 | padding: 0; |
4 | list-style-type: none; |
5 | } |
6 | |
7 | .md-list .md-one-line, |
8 | .md-list .md-two-line, |
9 | .md-list .md-three-line { |
10 | position: relative; |
11 | display: flex; |
12 | box-sizing: border-box; |
13 | } |
14 | |
15 | /* container */ |
16 | .md-list .md-one-line::before, |
17 | .md-list .md-two-line::before, |
18 | .md-list .md-three-line::before { |
19 | content: ""; |
20 | position: absolute; |
21 | z-index: -2; |
22 | top: 0; |
23 | right: 0; |
24 | left: 0; |
25 | bottom: 0; |
26 | background-color: var(--md-sys-color-surface); |
27 | } |
28 | |
29 | /* state layer */ |
30 | .md-list .md-one-line::after, |
31 | .md-list .md-two-line::after, |
32 | .md-list .md-three-line::after { |
33 | content: ""; |
34 | position: absolute; |
35 | z-index: -1; |
36 | top: 0; |
37 | right: 0; |
38 | left: 0; |
39 | bottom: 0; |
40 | background-color: transparent; |
41 | } |
42 | |
43 | .md-list .md-one-line { |
44 | align-items: center; |
45 | gap: 1rem; |
46 | min-height: 3.5rem; |
47 | padding: 0.5rem 1.5rem 0.5rem 1rem; |
48 | } |
49 | |
50 | .md-list .md-one-line.video, |
51 | .md-list .md-two-line.video { |
52 | padding: 0.75rem 1.5rem 0.75rem 0; |
53 | } |
54 | |
55 | .md-list .md-two-line, |
56 | .md-list .md-three-line { |
57 | flex-flow: column; |
58 | } |
59 | |
60 | .md-list .md-two-line { |
61 | justify-content: center; |
62 | min-height: 4.5rem; |
63 | padding: 0.5rem 1.5rem 0.5rem 1rem; |
64 | } |
65 | |
66 | .md-list .md-two-line.icon, |
67 | .md-list .md-two-line.avatar, |
68 | .md-list .md-two-line.image, |
69 | .md-list .md-two-line.video, |
70 | .md-list .md-three-line.icon, |
71 | .md-list .md-three-line.avatar, |
72 | .md-list .md-three-line.image, |
73 | .md-list .md-three-line.video { |
74 | display: grid; |
75 | column-gap: 1rem; |
76 | row-gap: 0; |
77 | grid-template-areas: |
78 | "img headline" |
79 | "img supporting"; |
80 | } |
81 | |
82 | .md-list .md-two-line.icon, |
83 | .md-list .md-two-line.avatar, |
84 | .md-list .md-two-line.image, |
85 | .md-list .md-two-line.video { |
86 | align-content: center; |
87 | grid-template-rows: 1fr 1fr; |
88 | } |
89 | |
90 | .md-list .md-two-line.icon, |
91 | .md-list .md-three-line.icon { |
92 | grid-template-columns: var(--iconSize) 1fr; |
93 | } |
94 | |
95 | .md-list .md-two-line.avatar, |
96 | .md-list .md-three-line.avatar { |
97 | grid-template-columns: var(--avatarSize) 1fr; |
98 | } |
99 | |
100 | .md-list .md-two-line.image, |
101 | .md-list .md-three-line.image { |
102 | grid-template-columns: var(--imageSize) 1fr; |
103 | } |
104 | |
105 | .md-list .md-two-line.video, |
106 | .md-list .md-three-line.video { |
107 | grid-template-columns: var(--videoWidth) 1fr; |
108 | } |
109 | |
110 | .md-list .md-three-line { |
111 | align-content: flex-start; |
112 | min-height: 5.5rem; |
113 | padding: 0.75rem 1.5rem 0.75rem 1rem; |
114 | } |
115 | |
116 | .md-list .md-three-line.video { |
117 | padding: 0.75rem 1.5rem 0.75rem 0; |
118 | } |
119 | |
120 | .md-list .md-three-line.icon, |
121 | .md-list .md-three-line.avatar, |
122 | .md-list .md-three-line.image, |
123 | .md-list .md-three-line.video { |
124 | align-content: start; |
125 | grid-template-rows: var(--md-sys-typescale-label-large-line-height) 1fr; |
126 | } |
127 | |
128 | /* state layer */ |
129 | .md-list .md-one-line:hover::after, |
130 | .md-list .md-two-line:hover::after, |
131 | .md-list .md-three-line:hover::after { |
132 | background-color: var(--md-sys-color-on-surface); |
133 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
134 | } |
135 | |
136 | /* state layer */ |
137 | .md-list a.md-one-line:focus::after, |
138 | .md-list a.md-two-line:focus::after, |
139 | .md-list a.md-three-line:focus::after, |
140 | .md-list a.md-one-line:focus-visible::after, |
141 | .md-list a.md-two-line:focus-visible::after, |
142 | .md-list a.md-three-line:focus-visible::after { |
143 | background-color: var(--md-sys-color-on-surface); |
144 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
145 | } |
146 | |
147 | .md-list a:focus, |
148 | .md-list a:focus-visible { |
149 | outline: none; |
150 | } |
151 | |
152 | .md-list a:active { |
153 | background-position: center; |
154 | background-image: |
155 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
156 | background-size: 100%; |
157 | animation-name: md-ripple; |
158 | animation-duration: var(--md-sys-motion-duration-500); |
159 | box-shadow: var(--md-box_shadow_level0) !important; |
160 | } |
161 | |
162 | /* state layer */ |
163 | .md-list a.md-one-line:active::after, |
164 | .md-list a.md-two-line:active::after, |
165 | .md-list a.md-three-line:active::after { |
166 | background-color: var(--md-sys-color-on-surface); |
167 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
168 | } |
169 | |
170 | .md-list a.md-two-line, |
171 | .md-list a.md-three-line { |
172 | text-decoration: none; |
173 | } |
174 | |
175 | .md-list a.md-two-line .headline, |
176 | .md-list a.md-three-line .headline { |
177 | text-decoration: underline; |
178 | } |
179 | |
180 | .md-list .headline { |
181 | grid-area: headline; |
182 | display: block; |
183 | box-sizing: border-box; |
184 | color: var(--md-sys-color-on-surface); |
185 | font-family: var(--md-sys-typescale-body-large-font); |
186 | font-weight: var(--md-sys-typescale-body-large-weight); |
187 | font-size: var(--md-sys-typescale-body-large-size); |
188 | font-style: var(--md-sys-typescale-body-large-font-style); |
189 | letter-spacing: var(--md-sys-typescale-body-large-tracking); |
190 | line-height: var(--md-sys-typescale-body-large-line-height); |
191 | text-transform: var(--md-sys-typescale-body-large-text-transform); |
192 | text-decoration: var(--md-sys-typescale-body-large-text-decoration); |
193 | max-height: var(--md-sys-typescale-body-large-line-height); |
194 | white-space: nowrap; |
195 | text-overflow: ellipsis; |
196 | overflow: hidden; |
197 | } |
198 | |
199 | .md-list .md-two-line.icon .headline, |
200 | .md-list .md-two-line.avatar .headline, |
201 | .md-list .md-two-line.image .headline, |
202 | .md-list .md-two-line.video .headline, |
203 | .md-list .md-three-line.icon .headline, |
204 | .md-list .md-three-line.avatar .headline, |
205 | .md-list .md-three-line.image .headline, |
206 | .md-list .md-three-line.video .headline { |
207 | align-self: end; |
208 | } |
209 | |
210 | .md-list .supporting { |
211 | grid-area: supporting; |
212 | display: -webkit-box; |
213 | -webkit-box-orient: vertical; |
214 | overflow: hidden; |
215 | box-sizing: border-box; |
216 | align-self: start; |
217 | font-family: var(--md-sys-typescale-body-medium-font); |
218 | font-weight: var(--md-sys-typescale-body-medium-weight); |
219 | font-size: var(--md-sys-typescale-body-medium-size); |
220 | font-style: var(--md-sys-typescale-body-medium-font-style); |
221 | letter-spacing: var(--md-sys-typescale-body-medium-tracking); |
222 | line-height: var(--md-sys-typescale-body-medium-line-height); |
223 | text-transform: var(--md-sys-typescale-body-medium-text-transform); |
224 | text-decoration: var(--md-sys-typescale-body-medium-text-decoration); |
225 | } |
226 | |
227 | .md-list .md-two-line .supporting { |
228 | max-height: var(--md-sys-typescale-body-medium-line-height); |
229 | line-clamp: 1; |
230 | -webkit-line-clamp: 1; |
231 | } |
232 | |
233 | .md-list .md-three-line .supporting { |
234 | max-height: calc(2 * var(--md-sys-typescale-body-medium-line-height)); |
235 | line-clamp: 2; |
236 | -webkit-line-clamp: 2; |
237 | } |
238 | |
239 | .md-list .avatar img, |
240 | .md-list .avatar label, |
241 | .md-list .avatar .material-symbols-outlined:first-child { |
242 | flex-shrink: 0; |
243 | background-color: var(--md-sys-color-primary-container); |
244 | color: var(--md-sys-color-on-primary-container); |
245 | border-radius: 50%; |
246 | width: var(--avatarSize); |
247 | height: var(--avatarSize); |
248 | } |
249 | |
250 | .md-list .avatar label { |
251 | display: inline-block; |
252 | font-family: var(--md-sys-typescale-title-medium-font); |
253 | font-weight: var(--md-sys-typescale-title-medium-weight); |
254 | font-size: var(--md-sys-typescale-title-medium-size); |
255 | font-style: var(--md-sys-typescale-title-medium-font-style); |
256 | letter-spacing: var(--md-sys-typescale-title-medium-tracking); |
257 | line-height: var(--md-sys-typescale-title-medium-line-height); |
258 | text-transform: var(--md-sys-typescale-title-medium-text-transform); |
259 | text-decoration: var(--md-sys-typescale-title-medium-text-decoration); |
260 | overflow: hidden; |
261 | } |
262 | |
263 | .md-list .avatar .material-symbols-outlined:first-child { |
264 | font-size: var(--avatarSize); |
265 | } |
266 | |
267 | .md-list .avatar.md-two-line img, |
268 | .md-list .avatar.md-two-line label, |
269 | .md-list .avatar.md-two-line .material-symbols-outlined:first-child { |
270 | grid-area: img; |
271 | align-self: center; |
272 | } |
273 | |
274 | .md-list .avatar.md-three-line img, |
275 | .md-list .avatar.md-three-line label, |
276 | .md-list .avatar.md-three-line .material-symbols-outlined:first-child { |
277 | grid-area: img; |
278 | align-self: start; |
279 | } |
280 | |
281 | .md-list .icon img, |
282 | .md-list .icon .material-symbols-outlined:first-child { |
283 | flex-shrink: 0; |
284 | color: var(--md-sys-color-on-surface-variant); |
285 | width: var(--iconSize); |
286 | height: var(--iconSize); |
287 | } |
288 | |
289 | .md-list .icon .material-symbols-outlined:first-child { |
290 | font-size: var(--iconSize); |
291 | } |
292 | |
293 | .md-list .icon.md-two-line img, |
294 | .md-list .icon.md-two-line .material-symbols-outlined:first-child { |
295 | grid-area: img; |
296 | align-self: center; |
297 | } |
298 | |
299 | .md-list .icon.md-three-line img, |
300 | .md-list .icon.md-three-line .material-symbols-outlined:first-child { |
301 | grid-area: img; |
302 | align-self: start; |
303 | } |
304 | |
305 | .md-list .video img { |
306 | flex-shrink: 0; |
307 | color: var(--md-sys-color-on-surface-variant); |
308 | width: var(--videoWidth); |
309 | height: var(--videoHeight); |
310 | } |
311 | |
312 | .md-list .video.md-two-line img { |
313 | grid-area: img; |
314 | align-self: center; |
315 | } |
316 | |
317 | .md-list .video.md-three-line img { |
318 | grid-area: img; |
319 | align-self: start; |
320 | } |
321 | |
322 | .md-list .image img, |
323 | .md-list .image .material-symbols-outlined:first-child { |
324 | flex-shrink: 0; |
325 | color: var(--md-sys-color-on-surface-variant); |
326 | width: var(--imageSize); |
327 | height: var(--imageSize); |
328 | } |
329 | |
330 | .md-list .image .material-symbols-outlined:first-child { |
331 | font-size: var(--imageSize); |
332 | } |
333 | |
334 | .md-list .image.md-two-line img, |
335 | .md-list .image.md-two-line .material-symbols-outlined:first-child { |
336 | grid-area: img; |
337 | align-self: center; |
338 | } |
339 | |
340 | .md-list .image.md-three-line img, |
341 | .md-list .image.md-three-line .material-symbols-outlined:first-child { |
342 | grid-area: img; |
343 | align-self: start; |
344 | } |
1 | .md-menu { |
2 | display: none; |
3 | z-index: 2; |
4 | box-sizing: border-box; |
5 | cursor: default; |
6 | padding: 0.25rem 0; |
7 | border-radius: |
8 | var(--md-sys-shape-corner-extra-small-default-size); |
9 | background-color: var(--md-sys-color-surface-container-low); |
10 | box-shadow: var(--md-box_shadow_level2); |
11 | transform: translateY(-50%) scaleY(0); |
12 | transition-timing-function: |
13 | cubic-bezier(var(--md-sys-motion-easing-standard-x0), |
14 | var(--md-sys-motion-easing-standard-y0), |
15 | var(--md-sys-motion-easing-standard-x1), |
16 | var(--md-sys-motion-easing-standard-y1)); |
17 | transition-property: display, transform; |
18 | transition-behavior: allow-discrete; |
19 | transition-duration: var(--md-sys-motion-duration-500); |
20 | } |
21 | |
22 | .md-menu.open { |
23 | display: block; |
24 | transform: translateY(0) scaleY(1); |
25 | } |
26 | |
27 | @starting-style { |
28 | .md-menu.open { |
29 | display: block; |
30 | transform: translateY(-50%) scaleY(0); |
31 | } |
32 | } |
33 | |
34 | /* container */ |
35 | .md-menu>*::after { |
36 | content: ""; |
37 | position: absolute; |
38 | z-index: -2; |
39 | top: 0; |
40 | right: 0; |
41 | left: 0; |
42 | bottom: 0; |
43 | } |
44 | |
45 | /* container */ |
46 | .md-menu>.selected::after { |
47 | background-color: var(--md-sys-color-secondary-container); |
48 | } |
49 | |
50 | /* label, shape */ |
51 | .md-menu>* { |
52 | position: relative; |
53 | display: block; |
54 | box-sizing: border-box; |
55 | height: 3rem; |
56 | line-height: 3rem; |
57 | padding: 0 0.75rem; |
58 | color: var(--md-sys-color-on-surface); |
59 | font-family: var(--md-sys-typescale-label-large-font); |
60 | font-weight: var(--md-sys-typescale-label-large-weight); |
61 | font-size: var(--md-sys-typescale-label-large-size); |
62 | font-style: var(--md-sys-typescale-label-large-font-style); |
63 | letter-spacing: var(--md-sys-typescale-label-large-tracking); |
64 | text-transform: var(--md-sys-typescale-label-large-text-transform); |
65 | text-decoration: var(--md-sys-typescale-label-large-text-decoration); |
66 | white-space: nowrap; |
67 | text-overflow: ellipsis; |
68 | overflow: hidden; |
69 | } |
70 | |
71 | /* label, shape */ |
72 | .md-menu>.selected { |
73 | color: var(--md-sys-color-on-secondary-container); |
74 | } |
75 | |
76 | /* state layer */ |
77 | .md-menu>*::before { |
78 | content: ""; |
79 | position: absolute; |
80 | z-index: -1; |
81 | top: 0; |
82 | right: 0; |
83 | left: 0; |
84 | bottom: 0; |
85 | } |
86 | |
87 | /* icon */ |
88 | .md-menu>* span { |
89 | position: relative; |
90 | margin-right: 0.75rem; |
91 | vertical-align: middle; |
92 | color: var(--md-sys-color-on-surface-variant); |
93 | font-size: 1.5rem; |
94 | width: 1.5rem; |
95 | height: 1.5rem; |
96 | } |
97 | |
98 | /* icon */ |
99 | .md-menu>.selected span { |
100 | color: var(--md-sys-color-on-secondary-container); |
101 | } |
102 | |
103 | /* state layer */ |
104 | .md-menu>:hover::before { |
105 | background-color: var(--md-sys-color-on-surface); |
106 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
107 | } |
108 | |
109 | /* label, shape */ |
110 | .md-menu>:hover { |
111 | color: var(--md-sys-color-on-surface); |
112 | } |
113 | |
114 | /* icon */ |
115 | .md-menu>:hover span { |
116 | color: var(--md-sys-color-on-surface-variant); |
117 | } |
118 | |
119 | /* state layer */ |
120 | .md-menu>:focus::before { |
121 | background-color: var(--md-sys-color-on-surface); |
122 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
123 | } |
124 | |
125 | /* label, shape */ |
126 | .md-menu>:focus { |
127 | color: var(--md-sys-color-on-surface); |
128 | outline: none; |
129 | } |
130 | |
131 | /* icon */ |
132 | .md-menu>:focus span { |
133 | color: var(--md-sys-color-on-surface-variant); |
134 | } |
135 | |
136 | /* label, shape */ |
137 | .md-menu>:active { |
138 | background-position: center; |
139 | background-image: |
140 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
141 | background-size: 100%; |
142 | animation-name: md-ripple; |
143 | animation-duration: var(--md-sys-motion-duration-500); |
144 | color: var(--md-sys-color-on-surface); |
145 | } |
146 | |
147 | /* state layer */ |
148 | .md-menu>:active::before { |
149 | background-color: var(--md-sys-color-on-surface); |
150 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
151 | } |
152 | |
153 | |
154 | /* icon */ |
155 | .md-menu>:active span { |
156 | color: var(--md-sys-color-on-surface-variant); |
157 | } |
158 | |
159 | .md-menu input[type="radio"] { |
160 | appearance: none; |
161 | transform: scaleX(0); |
162 | } |
1 | .md-navigation-bar { |
2 | display: flex; |
3 | justify-content: center; |
4 | align-items: stretch; |
5 | position: fixed; |
6 | left: 0; |
7 | right: 0; |
8 | bottom: 0; |
9 | background-color: var(--md-sys-color-surface-container-low); |
10 | } |
11 | |
12 | .md-navigation-bar a { |
13 | position: relative; |
14 | display: block; |
15 | flex: 0 1 auto; |
16 | color: var(--md-sys-color-on-surface-variant); |
17 | font-family: var(--md-sys-typescale-label-medium-font); |
18 | font-weight: var(--md-sys-typescale-label-medium-weight); |
19 | font-size: var(--md-sys-typescale-label-medium-size); |
20 | font-style: var(--md-sys-typescale-label-medium-font-style); |
21 | letter-spacing: var(--md-sys-typescale-label-medium-tracking); |
22 | line-height: var(--md-sys-typescale-label-medium-line-height); |
23 | text-transform: var(--md-sys-typescale-label-medium-text-transform); |
24 | text-decoration: var(--md-sys-typescale-label-medium-text-decoration); |
25 | text-decoration: none; |
26 | padding-top: 0.75rem; |
27 | padding-bottom: 1rem; |
28 | padding-left: 0.25rem; |
29 | padding-right: 0.25rem; |
30 | text-align: center; |
31 | overflow: hidden; |
32 | box-sizing: border-box; |
33 | } |
34 | |
35 | .md-navigation-bar a.active { |
36 | color: var(--md-sys-color-on-surface); |
37 | } |
38 | |
39 | /* state layer */ |
40 | .md-navigation-bar a::after { |
41 | content: ""; |
42 | position: absolute; |
43 | z-index: -2; |
44 | top: 0; |
45 | right: 0; |
46 | left: 0; |
47 | bottom: 0; |
48 | background-color: transparent; |
49 | } |
50 | |
51 | /* state layer */ |
52 | .md-navigation-bar a:hover::after { |
53 | background-color: var(--md-sys-color-on-surface-variant); |
54 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
55 | } |
56 | |
57 | /* state layer */ |
58 | .md-navigation-bar a.active:hover::after { |
59 | background-color: var(--md-sys-color-on-surface); |
60 | } |
61 | |
62 | .md-navigation-bar a:focus { |
63 | outline: none; |
64 | } |
65 | |
66 | /* state layer */ |
67 | .md-navigation-bar a:focus::after { |
68 | background-color: var(--md-sys-color-on-surface-variant); |
69 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
70 | } |
71 | |
72 | /* state layer */ |
73 | .md-navigation-bar a.active:focus::after { |
74 | background-color: var(--md-sys-color-on-surface); |
75 | } |
76 | |
77 | .md-navigation-bar a:active { |
78 | background-position: center; |
79 | background-image: |
80 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
81 | background-size: 100%; |
82 | animation-name: md-ripple; |
83 | animation-duration: var(--md-sys-motion-duration-500); |
84 | } |
85 | |
86 | /* state layer */ |
87 | .md-navigation-bar a:active::after { |
88 | background-color: var(--md-sys-color-on-surface-variant); |
89 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
90 | } |
91 | |
92 | /* state layer */ |
93 | .md-navigation-bar a.active:active::after { |
94 | background-color: var(--md-sys-color-on-surface); |
95 | } |
96 | |
97 | .md-navigation-bar span { |
98 | color: var(--md-sys-color-on-surface-variant); |
99 | position: relative; |
100 | display: block; |
101 | height: 2rem; |
102 | width: 4rem; |
103 | line-height: 2rem; |
104 | margin-bottom: 0.25rem; |
105 | box-sizing: border-box; |
106 | margin-left: auto; |
107 | margin-right: auto; |
108 | overflow: hidden; |
109 | } |
110 | |
111 | .md-navigation-bar a.active span { |
112 | color: var(--md-sys-color-on-secondary-container); |
113 | } |
114 | |
115 | .md-navigation-bar a.active { |
116 | font-variation-settings: 'FILL'1, 'wght'700, 'GRAD'0, 'opsz'48; |
117 | } |
118 | |
119 | .md-navigation-bar span::before { |
120 | content: ""; |
121 | background-color: var(--md-sys-color-secondary-container); |
122 | position: absolute; |
123 | z-index: -1; |
124 | top: 0; |
125 | left: 0; |
126 | height: 2rem; |
127 | width: 4rem; |
128 | border-radius: 1rem; |
129 | box-sizing: border-box; |
130 | transform: scaleX(0); |
131 | transition-property: transform; |
132 | transition-duration: var(--md-sys-motion-duration-500); |
133 | } |
134 | |
135 | .md-navigation-bar a.active span::before, |
136 | .md-navigation-bar a:active span::before { |
137 | transform: scaleX(1); |
138 | } |
1 | .md-outline-button { |
2 | position: relative; |
3 | box-sizing: border-box; |
4 | border-radius: 1.25rem; |
5 | height: 2.5rem; |
6 | padding: 0 1.5rem; |
7 | border: 0.0625rem solid var(--md-sys-color-outline); |
8 | background-color: transparent; |
9 | box-shadow: var(--md-box_shadow_level0); |
10 | font-family: var(--md-sys-typescale-label-large-font); |
11 | font-weight: var(--md-sys-typescale-label-large-weight); |
12 | font-size: var(--md-sys-typescale-label-large-size); |
13 | font-style: var(--md-sys-typescale-label-large-font-style); |
14 | letter-spacing: var(--md-sys-typescale-label-large-tracking); |
15 | text-transform: var(--md-sys-typescale-label-large-text-transform); |
16 | text-decoration: var(--md-sys-typescale-label-large-text-decoration); |
17 | color: var(--md-sys-color-primary); |
18 | white-space: nowrap; |
19 | text-overflow: ellipsis; |
20 | overflow: hidden; |
21 | } |
22 | |
23 | /* state layer */ |
24 | .md-outline-button::after { |
25 | content: ""; |
26 | position: absolute; |
27 | z-index: -1; |
28 | top: 0; |
29 | right: 0; |
30 | left: 0; |
31 | bottom: 0; |
32 | background-color: transparent; |
33 | } |
34 | |
35 | .md-outline-button:hover { |
36 | color: var(--md-sys-color-primary); |
37 | border-color: var(--md-sys-color-outline); |
38 | } |
39 | |
40 | /* state layer */ |
41 | .md-outline-button:hover::after { |
42 | background-color: var(--md-sys-color-primary); |
43 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
44 | } |
45 | |
46 | .md-outline-button:focus { |
47 | outline: none; |
48 | color: var(--md-sys-color-primary); |
49 | border-color: var(--md-sys-color-outline); |
50 | } |
51 | |
52 | /* state layer */ |
53 | .md-outline-button:focus::after { |
54 | background-color: var(--md-sys-color-primary); |
55 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
56 | } |
57 | |
58 | .md-outline-button:active { |
59 | color: var(--md-sys-color-primary); |
60 | border-color: var(--md-sys-color-outline); |
61 | background-position: center; |
62 | background-image: |
63 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
64 | background-size: 100%; |
65 | animation-name: md-ripple; |
66 | animation-duration: var(--md-sys-motion-duration-500); |
67 | box-shadow: var(--md-box_shadow_level0) !important; |
68 | } |
69 | |
70 | /* state layer */ |
71 | .md-outline-button:active::after { |
72 | background-color: var(--md-sys-color-primary); |
73 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
74 | } |
75 | |
76 | .md-outline-button:disabled { |
77 | background-color: transparent !important; |
78 | border-color: var(--md-sys-color-on-surface) !important; |
79 | color: var(--md-sys-color-on-surface) !important; |
80 | opacity: 0.38 !important; |
81 | } |
82 | |
83 | /* container */ |
84 | .md-outline-button:disabled::after { |
85 | background-color: transparent !important; |
86 | opacity: 1 !important; |
87 | } |
1 | @keyframes md-ripple { |
2 | |
3 | from { |
4 | background-size: 100%; |
5 | } |
6 | |
7 | to { |
8 | background-size: 15000%; |
9 | } |
10 | |
11 | } |
1 | .md-segmented-button { |
2 | display: flex; |
3 | align-items: stretch; |
4 | box-sizing: border-box; |
5 | border: 0.0625rem solid var(--md-sys-color-outline); |
6 | height: 2.5rem; |
7 | border-radius: 1.25rem; |
8 | overflow: hidden; |
9 | } |
10 | |
11 | .md-segmented-button[hidden] { |
12 | display: none; |
13 | } |
14 | |
15 | .md-segmented-button input { |
16 | -webkit-appearance: none; |
17 | appearance: none; |
18 | flex: 0 1 0.0625rem; |
19 | width: 0.0625rem; |
20 | height: 2.375rem; |
21 | margin: 0; |
22 | padding: 0; |
23 | background-color: var(--md-sys-color-outline); |
24 | } |
25 | |
26 | .md-segmented-button input:first-of-type { |
27 | transform: scaleX(0); |
28 | } |
29 | |
30 | .md-segmented-button input:focus { |
31 | outline: none; |
32 | } |
33 | |
34 | .md-segmented-button :checked+label { |
35 | color: var(--md-sys-color-on-secondary-container); |
36 | } |
37 | |
38 | .md-segmented-button label { |
39 | position: relative; |
40 | flex: 1 1 1.5rem; |
41 | display: block; |
42 | box-sizing: border-box; |
43 | height: 2.375rem; |
44 | line-height: 2.375rem; |
45 | text-align: center; |
46 | color: var(--md-sys-color-on-surface); |
47 | font-family: var(--md-sys-typescale-label-large-font); |
48 | font-weight: var(--md-sys-typescale-label-large-weight); |
49 | font-size: var(--md-sys-typescale-label-large-size); |
50 | font-style: var(--md-sys-typescale-label-large-font-style); |
51 | letter-spacing: var(--md-sys-typescale-label-large-tracking); |
52 | text-transform: var(--md-sys-typescale-label-large-text-transform); |
53 | text-decoration: var(--md-sys-typescale-label-large-text-decoration); |
54 | padding: 0 0.75rem; |
55 | overflow: hidden; |
56 | white-space: nowrap; |
57 | text-overflow: ellipsis; |
58 | overflow: hidden; |
59 | } |
60 | |
61 | .md-segmented-button label::before { |
62 | /* container */ |
63 | content: ""; |
64 | position: absolute; |
65 | z-index: -2; |
66 | top: 0; |
67 | right: 0; |
68 | left: 0; |
69 | bottom: 0; |
70 | } |
71 | |
72 | .md-segmented-button label::after { |
73 | /* state layer */ |
74 | content: ""; |
75 | position: absolute; |
76 | z-index: -1; |
77 | top: 0; |
78 | right: 0; |
79 | left: 0; |
80 | bottom: 0; |
81 | background-color: transparent; |
82 | } |
83 | |
84 | .md-segmented-button :checked+label::before { |
85 | /* container */ |
86 | background-color: var(--md-sys-color-secondary-container); |
87 | } |
88 | |
89 | .md-segmented-button label span { |
90 | vertical-align: middle; |
91 | color: var(--md-sys-color-on-surface); |
92 | font-size: 1.125rem; |
93 | width: 1.125rem; |
94 | height: 1.125rem; |
95 | margin-right: 0.5rem; |
96 | } |
97 | |
98 | |
99 | .md-segmented-button label span:first-child { |
100 | display: none; |
101 | } |
102 | |
103 | .md-segmented-button :checked+label span:first-child { |
104 | display: inline-block; |
105 | color: var(--md-sys-color-on-secondary-container); |
106 | } |
107 | |
108 | /* state layer */ |
109 | .md-segmented-button label:hover::after { |
110 | background-color: var(--md-sys-color-on-surface); |
111 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
112 | } |
113 | |
114 | /* state layer */ |
115 | .md-segmented-button :checked+label:hover::after { |
116 | background-color: var(--md-sys-color-on-secondary-container); |
117 | } |
118 | |
119 | /* state layer */ |
120 | .md-segmented-button :focus+label::after { |
121 | background-color: var(--md-sys-color-on-surface); |
122 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
123 | } |
124 | |
125 | /* state layer */ |
126 | .md-segmented-button :focus:checked+label::after { |
127 | background-color: var(--md-sys-color-on-secondary-container); |
128 | } |
129 | |
130 | .md-segmented-button label:active, |
131 | .md-segmented-button :active+label { |
132 | background-position: center; |
133 | background-image: |
134 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
135 | background-size: 100%; |
136 | animation-name: md-ripple; |
137 | animation-duration: var(--md-sys-motion-duration-500); |
138 | } |
139 | |
140 | /* state layer */ |
141 | .md-segmented-button :active+label::after { |
142 | background-color: var(--md-sys-color-on-surface); |
143 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
144 | } |
145 | |
146 | /* state layer */ |
147 | .md-segmented-button :active:checked+label::after { |
148 | background-color: var(--md-sys-color-on-secondary-container); |
149 | } |
1 | md-slider-field input::-webkit-slider-runnable-track { |
2 | height: 0.25rem; |
3 | border-radius: 0.125rem; |
4 | } |
5 | |
6 | md-slider-field input::-webkit-slider-thumb { |
7 | -webkit-appearance: none; |
8 | background-color: var(--md-sys-color-primary); |
9 | width: 1.25rem; |
10 | height: 1.25rem; |
11 | border-radius: 0.625rem; |
12 | margin-top: -0.5625rem; |
13 | } |
14 | |
15 | md-slider-field input:hover::-webkit-slider-thumb { |
16 | box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-hover-transparency-percentage)); |
17 | } |
18 | |
19 | md-slider-field input:focus::-webkit-slider-thumb { |
20 | box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-focus-transparency-percentage)); |
21 | } |
22 | |
23 | md-slider-field input:active::-webkit-slider-thumb { |
24 | box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-pressed-transparency-percentage)) !important; |
25 | background-position: center; |
26 | background-image: |
27 | radial-gradient(circle, var(--md-sys-color-primary-container) 1%, transparent 1%); |
28 | background-size: 100%; |
29 | animation-name: md-ripple; |
30 | animation-duration: var(--md-sys-motion-duration-500); |
31 | } |
32 | |
33 | md-slider-field.material::-moz-range-track { |
34 | height: 0.25rem; |
35 | border-radius: 0.125rem; |
36 | } |
37 | |
38 | md-slider-field input::-moz-range-thumb { |
39 | -webkit-appearance: none; |
40 | appearance: none; |
41 | background-color: var(--md-sys-color-primary); |
42 | width: 1.25rem; |
43 | height: 1.25rem; |
44 | border: none; |
45 | border-radius: 0.625rem; |
46 | } |
47 | |
48 | md-slider-field input:hover::-moz-range-thumb { |
49 | box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-hover-transparency-percentage)); |
50 | } |
51 | |
52 | md-slider-field input:focus::-moz-range-thumb { |
53 | box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-focus-transparency-percentage)); |
54 | } |
55 | |
56 | md-slider-field input:active::-moz-range-thumb { |
57 | box-shadow: 0 0 0 0.625rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-pressed-transparency-percentage)) !important; |
58 | background-position: center; |
59 | background-image: |
60 | radial-gradient(circle, var(--md-sys-color-primary-container) 1%, transparent 1%); |
61 | background-size: 100%; |
62 | animation-name: md-ripple; |
63 | animation-duration: var(--md-sys-motion-duration-500); |
64 | } |
1 | .md-standard-icon-button { |
2 | position: relative; |
3 | display: inline-block; |
4 | border: none; |
5 | padding: 0.25rem; |
6 | background-color: transparent; |
7 | text-decoration: none; |
8 | border-radius: 50%; |
9 | overflow: hidden; |
10 | } |
11 | |
12 | .md-standard-icon-button[hidden] { |
13 | display: none; |
14 | } |
15 | |
16 | /* state layer */ |
17 | .md-standard-icon-button::after { |
18 | content: ""; |
19 | position: absolute; |
20 | top: 0.25rem; |
21 | right: 0.25rem; |
22 | left: 0.25rem; |
23 | bottom: 0.25rem; |
24 | border-radius: 50%; |
25 | } |
26 | |
27 | .md-standard-icon-button span { |
28 | position: relative; |
29 | padding: 0.5rem; |
30 | color: var(--md-sys-color-on-surface-variant); |
31 | font-size: 1.5rem; |
32 | width: 1.5rem; |
33 | height: 1.5rem; |
34 | } |
35 | |
36 | .md-standard-icon-button.avatar span { |
37 | padding: 0.3125rem; |
38 | font-size: 1.875rem; |
39 | width: 1.875rem; |
40 | height: 1.875rem; |
41 | } |
42 | |
43 | .md-standard-icon-button:hover::after { |
44 | background-color: var(--md-sys-color-on-surface-variant); |
45 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
46 | } |
47 | |
48 | .md-standard-icon-button:hover span { |
49 | color: var(--md-sys-color-on-surface-variant); |
50 | } |
51 | |
52 | .md-standard-icon-button:focus { |
53 | outline: none; |
54 | } |
55 | |
56 | .md-standard-icon-button:focus::after { |
57 | background-color: var(--md-sys-color-on-surface-variant); |
58 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
59 | } |
60 | |
61 | .md-standard-icon-button:focus span { |
62 | color: var(--md-sys-color-on-surface-variant); |
63 | } |
64 | |
65 | .md-standard-icon-button:active { |
66 | background-position: center; |
67 | background-image: |
68 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
69 | background-size: 100%; |
70 | animation-name: md-ripple; |
71 | animation-duration: var(--md-sys-motion-duration-500); |
72 | } |
73 | |
74 | .md-standard-icon-button:active::after { |
75 | background-color: var(--md-sys-color-on-surface-variant); |
76 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
77 | } |
78 | |
79 | .md-standard-icon-button:active span { |
80 | -color: var(--md-sys-color-on-surface-variant); |
81 | } |
82 | |
83 | .md-standard-icon-button:disabled::after { |
84 | background-color: transparent !important; |
85 | opacity: 1; |
86 | } |
87 | |
88 | .md-standard-icon-button:disabled span { |
89 | color: var(--md-sys-color-on-surface) !important; |
90 | opacity: 0.38; |
91 | } |
92 | |
93 | .md-standard-icon-button:disabled:active { |
94 | background-image: none; |
95 | animation-name: none; |
96 | animation-duration: unset; |
97 | } |
1 | .md-switch { |
2 | -webkit-appearance: none; |
3 | appearance: none; |
4 | position: relative; |
5 | display: inline-block; |
6 | vertical-align: middle; |
7 | box-sizing: content-box; |
8 | padding: 0; |
9 | padding-block: 0; |
10 | padding-inline: 0; |
11 | } |
12 | |
13 | .md-switch:focus { |
14 | outline: none; |
15 | } |
16 | |
17 | /* Track */ |
18 | body.material .md-switch { |
19 | width: 3rem; |
20 | height: 1.75rem; |
21 | border-radius: 1rem; |
22 | border: 0.125rem solid var(--md-sys-color-outline); |
23 | background-color: var(--md-sys-color-surface-container-highest); |
24 | } |
25 | |
26 | body.material .md-switch:checked { |
27 | background-color: var(--md-sys-color-primary); |
28 | } |
29 | |
30 | /* State */ |
31 | body.material .md-switch::before { |
32 | content: ""; |
33 | display: none; |
34 | position: absolute; |
35 | height: 2.5rem; |
36 | width: 2.5rem; |
37 | border-radius: 1.25rem; |
38 | top: -0.375rem; |
39 | left: -0.375rem; |
40 | } |
41 | |
42 | body.material .md-switch:checked:before { |
43 | left: auto; |
44 | right: -0.375rem; |
45 | } |
46 | |
47 | /* Handle */ |
48 | body.material .md-switch::after { |
49 | content: ""; |
50 | display: inline-block; |
51 | position: absolute; |
52 | transition-property: all; |
53 | transition-duration: var(--md-sys-motion-duration-700); |
54 | height: 1rem; |
55 | width: 1rem; |
56 | border-radius: 0.5rem; |
57 | top: 0.375rem; |
58 | left: 0.375rem; |
59 | background-color: var(--md-sys-color-outline); |
60 | box-shadow: var(--md-box_shadow_level1); |
61 | } |
62 | |
63 | body.material .md-switch:checked:after { |
64 | height: 1.5rem; |
65 | width: 1.5rem; |
66 | border-radius: 0.75rem; |
67 | top: 0.125rem; |
68 | left: auto; |
69 | right: 0.125rem; |
70 | background-color: var(--md-sys-color-on-primary); |
71 | } |
72 | |
73 | body.material .md-switch:hover::before { |
74 | display: inline-block; |
75 | background-color: var(--md-sys-color-on-surface); |
76 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
77 | } |
78 | |
79 | body.material .md-switch:checked:hover::before { |
80 | background-color: var(--md-sys-color-primary); |
81 | } |
82 | |
83 | body.material .md-switch:hover::after { |
84 | background-color: var(--md-sys-color-on-surface-variant); |
85 | } |
86 | |
87 | body.material .md-switch:checked:hover::after { |
88 | background-color: var(--md-sys-color-primary-container); |
89 | } |
90 | |
91 | body.material .md-switch:focus::before { |
92 | display: inline-block; |
93 | background-color: var(--md-sys-color-on-surface); |
94 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
95 | } |
96 | |
97 | body.material .md-switch:checked:focus::before { |
98 | background-color: var(--md-sys-color-primary); |
99 | } |
100 | |
101 | body.material .md-switch:focus::after { |
102 | background-color: var(--md-sys-color-on-surface-variant); |
103 | } |
104 | |
105 | body.material .md-switch:checked:focus::after { |
106 | background-color: var(--md-sys-color-primary-container); |
107 | } |
108 | |
109 | body.material .md-switch:active::before { |
110 | display: inline-block; |
111 | background-color: var(--md-sys-color-on-surface); |
112 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
113 | } |
114 | |
115 | |
116 | body.material .md-switch:checked:active::before { |
117 | background-color: var(--md-sys-color-primary); |
118 | } |
119 | |
120 | body.material .md-switch:active::after { |
121 | width: 1.75rem; |
122 | height: 1.75rem; |
123 | top: 0; |
124 | left: 0; |
125 | border-radius: 0.875rem; |
126 | background-position: center; |
127 | animation-name: md-ripple; |
128 | animation-duration: var(--md-sys-motion-duration-500); |
129 | background-size: 100%; |
130 | background-color: var(--md-sys-color-on-surface-variant); |
131 | background-image: |
132 | radial-gradient(circle, var(--md-sys-color-primary-container) 1%, transparent 1%); |
133 | box-shadow: var(--md-box_shadow_level1), 0 0 0 0.375rem color-mix(in srgb, var(--md-sys-color-on-surface), transparent var(--state-pressed-transparency-percentage)) !important; |
134 | } |
135 | |
136 | body.material .md-switch:checked:active::after { |
137 | left: auto; |
138 | right: 0; |
139 | background-color: var(--md-sys-color-primary-container); |
140 | background-image: |
141 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
142 | box-shadow: var(--md-box_shadow_level1), 0 0 0 0.375rem color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-pressed-transparency-percentage)) !important; |
143 | } |
144 | |
145 | body.apple .md-switch { |
146 | width: 3rem; |
147 | border-radius: 0.875rem; |
148 | height: 1.75rem; |
149 | background-color: var(--colIntIosOffBk); |
150 | } |
151 | |
152 | body.apple .md-switch:checked { |
153 | background-color: var(--colIntIosOnBk); |
154 | } |
155 | |
156 | body.apple .md-switch:focus { |
157 | background-color: var(--colIntIosOffBkFc); |
158 | } |
159 | |
160 | body.apple .md-switch:checked:focus { |
161 | background-color: var(--colIntIosOnBkFc); |
162 | } |
163 | |
164 | body.apple .md-switch::after { |
165 | content: ""; |
166 | display: inline-block; |
167 | position: absolute; |
168 | width: 1.5rem; |
169 | height: 1.5rem; |
170 | border-radius: 0.75rem; |
171 | top: 0.125rem; |
172 | left: 0.125rem; |
173 | background-color: var(--colIntIos); |
174 | } |
175 | |
176 | body.apple .md-switch:checked::after { |
177 | left: auto; |
178 | right: 0.125rem; |
179 | border-color: var(--colIntIosOnBk); |
180 | } |
181 | |
182 | body.apple .md-switch:focus::after { |
183 | border-color: var(--colIntIosOffBkFc); |
184 | } |
185 | |
186 | body.apple .md-switch:checked:focus::after { |
187 | border-color: var(--colIntIosOnBkFc); |
188 | } |
1 | .md-tab { |
2 | display: flex; |
3 | background-color: transparent; |
4 | align-items: stretch; |
5 | flex-wrap: nowrap; |
6 | overflow-x: auto; |
7 | position: sticky; |
8 | z-index: 1; |
9 | } |
10 | |
11 | .md-tab.fixed { |
12 | justify-content: center; |
13 | } |
14 | |
15 | .md-tab.scrollable { |
16 | padding-left: 2rem; |
17 | gap: 1rem; |
18 | } |
19 | |
20 | .md-tab.scroll { |
21 | background-color: var(--md-sys-color-surface-container-low); |
22 | } |
23 | |
24 | .md-tab a { |
25 | position: relative; |
26 | display: flex; |
27 | flex-direction: column; |
28 | justify-content: start; |
29 | align-items: center; |
30 | color: var(--md-sys-color-on-surface-variant); |
31 | font-family: var(--md-sys-typescale-title-small-font); |
32 | font-weight: var(--md-sys-typescale-title-small-weight); |
33 | font-size: var(--md-sys-typescale-title-small-size); |
34 | font-style: var(--md-sys-typescale-title-small-font-style); |
35 | letter-spacing: var(--md-sys-typescale-title-small-tracking); |
36 | line-height: var(--md-sys-typescale-title-small-line-height); |
37 | text-transform: var(--md-sys-typescale-title-small-text-transform); |
38 | text-decoration: var(--md-sys-typescale-title-small-text-decoration); |
39 | text-align: center; |
40 | box-sizing: border-box; |
41 | border-bottom: 0.1875rem solid var(--md-sys-color-surface); |
42 | } |
43 | |
44 | .md-tab.fixed a { |
45 | flex: 0 0 var(--tabWidth); |
46 | } |
47 | |
48 | .md-tab.scrollable a { |
49 | flex: 0 0 auto; |
50 | } |
51 | |
52 | .md-tab a.active { |
53 | border-bottom-color: var(--md-sys-color-primary); |
54 | } |
55 | |
56 | /* state layer */ |
57 | .md-tab a::after { |
58 | content: ""; |
59 | position: absolute; |
60 | z-index: -1; |
61 | top: 0; |
62 | right: 0; |
63 | left: 0; |
64 | bottom: 0; |
65 | background-color: transparent; |
66 | } |
67 | |
68 | .md-tab span { |
69 | font-size: var(--iconSize); |
70 | height: var(--iconSize); |
71 | width: var(--iconSize); |
72 | color: var(--md-sys-color-on-surface-variant); |
73 | } |
74 | |
75 | .md-tab .active span { |
76 | color: var(--md-sys-color-primary); |
77 | } |
78 | |
79 | .md-tab a:hover { |
80 | color: var(--md-sys-color-on-surface); |
81 | } |
82 | |
83 | /* state layer */ |
84 | .md-tab a:hover::after { |
85 | background-color: var(--md-sys-color-on-surface); |
86 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
87 | } |
88 | |
89 | .md-tab a.active:hover { |
90 | color: var(--md-sys-color-primary); |
91 | } |
92 | |
93 | /* state layer */ |
94 | .md-tab a.active:hover::after { |
95 | background-color: var(--md-sys-color-primary); |
96 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
97 | } |
98 | |
99 | .md-tab a:hover span { |
100 | color: var(--md-sys-color-on-surface); |
101 | } |
102 | |
103 | .md-tab a.active:hover span { |
104 | color: var(--md-sys-color-primary); |
105 | } |
106 | |
107 | .md-tab a:focus { |
108 | outline: none; |
109 | } |
110 | |
111 | /* state layer */ |
112 | .md-tab a:focus::after { |
113 | background-color: var(--md-sys-color-on-surface); |
114 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
115 | } |
116 | |
117 | /* state layer */ |
118 | .md-tab a.active:focus::after { |
119 | background-color: var(--md-sys-color-primary); |
120 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
121 | } |
122 | |
123 | .md-tab a:active { |
124 | background-position: center; |
125 | background-image: |
126 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
127 | background-size: 100%; |
128 | animation-name: md-ripple; |
129 | animation-duration: var(--md-sys-motion-duration-500); |
130 | } |
131 | |
132 | /* state layer */ |
133 | .md-tab a:active::after { |
134 | background-color: var(--md-sys-color-on-surface); |
135 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
136 | } |
137 | |
138 | /* state layer */ |
139 | .md-tab a.active:active::after { |
140 | background-color: var(--md-sys-color-primary); |
141 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
142 | } |
1 | .md-headline { |
2 | box-sizing: border-box; |
3 | margin: 0; |
4 | color: var(--md-sys-color-on-surface); |
5 | background-color: var(--md-sys-color-surface); |
6 | transition-property: color; |
7 | transition-duration: var(--md-sys-motion-duration-1000); |
8 | transition-timing-function: ease-in; |
9 | } |
10 | |
11 | .md-headline.scroll-adicional { |
12 | color: var(--md-sys-color-surface-container-low); |
13 | background-color: var(--md-sys-color-surface-container-low); |
14 | } |
15 | |
16 | .md-headline.scroll { |
17 | color: var(--md-sys-color-surface); |
18 | } |
19 | |
20 | .md-headline.headline-small { |
21 | padding: 0 1rem 1.5rem 1rem; |
22 | } |
23 | |
24 | .md-headline.headline-medium { |
25 | padding: 0 1rem 1.75rem 1rem; |
26 | } |
27 | |
28 | md-top-app-bar[headline] h1 { |
29 | opacity: 0; |
30 | transition-property: opacity; |
31 | transition-duration: var(--md-sys-motion-duration-1000); |
32 | } |
33 | |
34 | md-top-app-bar[headline].scroll h1 { |
35 | opacity: 1; |
36 | transition-timing-function: ease-in; |
37 | } |
1 | /* roboto-regular - latin */ |
2 | @font-face { |
3 | /* Revisa |
4 | * https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display |
5 | * para otras opciones. */ |
6 | font-display: swap; |
7 | font-family: 'Roboto'; |
8 | font-style: normal; |
9 | font-weight: 400; |
10 | /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */ |
11 | src: url('../fonts/roboto-v32-latin-regular.woff2') format('woff2'); |
12 | } |
1 | /** |
2 | * @param { HTMLElement } elementoHtml |
3 | */ |
4 | export function abreElementoHtml(elementoHtml) { |
5 | elementoHtml.classList.add("open") |
6 | } |
1 | /** |
2 | * @param { HTMLElement } elementoHtml |
3 | */ |
4 | export function cierraElementoHtmo(elementoHtml) { |
5 | elementoHtml.classList.remove("open") |
6 | } |
7 |
1 | /** |
2 | * Permite que los eventos de html usen la función. |
3 | * @param {function} functionInstance |
4 | */ |
5 | export function exportaAHtml(functionInstance) { |
6 | window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance |
7 | } |
8 | |
9 | /** |
10 | * @param {function} valor |
11 | */ |
12 | export function nombreDeFuncionParaHtml(valor) { |
13 | const names = valor.name.split(/\s+/g) |
14 | return names[names.length - 1] |
15 | } |
1 | /** |
2 | * @param {HTMLElement} elementoHtml |
3 | * @param {string} nombre |
4 | * @returns {string} |
5 | */ |
6 | export function getAttribute(elementoHtml, nombre) { |
7 | const valor = elementoHtml.getAttribute(nombre) |
8 | return valor === null ? "" : valor |
9 | } |
1 | /** |
2 | * Codifica un texto para que cambie los caracteres |
3 | * especiales y no se pueda interpretar como |
4 | * etiiqueta HTML. Esta técnica evita la inyección |
5 | * de código. |
6 | * @param { string } texto |
7 | * @returns { string } un texto que no puede |
8 | * interpretarse como HTML. */ |
9 | export function htmlentities(texto) { |
10 | return texto.replace(/[<>"']/g, textoDetectado => { |
11 | switch (textoDetectado) { |
12 | case "<": return "<" |
13 | case ">": return ">" |
14 | case '"': return """ |
15 | case "'": return "'" |
16 | default: return textoDetectado |
17 | } |
18 | }) |
19 | } |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Muestra un error en la consola y en un cuadro de |
6 | * alerta el mensaje de una excepción. |
7 | * @param { ProblemDetails | Error | null } error descripción del error. |
8 | */ |
9 | export function muestraError(error) { |
10 | |
11 | if (error === null) { |
12 | |
13 | console.error("Error") |
14 | alert("Error") |
15 | |
16 | } else if (error instanceof ProblemDetails) { |
17 | |
18 | let mensaje = error.title |
19 | if (error.detail) { |
20 | mensaje += `\n\n${error.detail}` |
21 | } |
22 | mensaje += `\n\nCódigo: ${error.status}` |
23 | if (error.type) { |
24 | mensaje += ` ${error.type}` |
25 | } |
26 | |
27 | console.error(mensaje) |
28 | console.error(error) |
29 | console.error("Headers:") |
30 | error.headers.forEach((valor, llave) => console.error(llave, "=", valor)) |
31 | alert(mensaje) |
32 | |
33 | } else { |
34 | |
35 | console.error(error) |
36 | alert(error.message) |
37 | |
38 | } |
39 | |
40 | } |
41 | |
42 | exportaAHtml(muestraError) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | |
3 | /** |
4 | * Si un elemento HTML tiene un mensaje de validación, lo |
5 | * muestra en su elemento de ayuda; en caso contrario, muestra |
6 | * un mensaje de ayuda. |
7 | * @param { { |
8 | * validity: { valid: boolean }; |
9 | * validationMessage: string |
10 | * } } elementoHtml elemento que contiene datos de validación. |
11 | * @param { HTMLElement } elementoDeAyuda elemento fonde |
12 | * se muestran los elementos de validación para elementoHtml. |
13 | * @param { string } mensajeDeAyuda mensaje de ayuda cuando el |
14 | * estado de elementoHtml es válido. |
15 | */ |
16 | export function muestraTextoDeAyuda(elementoHtml, elementoDeAyuda, |
17 | mensajeDeAyuda) { |
18 | if (elementoHtml.validity.valid) { |
19 | elementoDeAyuda.textContent = mensajeDeAyuda |
20 | } else { |
21 | elementoDeAyuda.textContent = elementoHtml.validationMessage |
22 | } |
23 | } |
24 | |
25 | exportaAHtml(muestraTextoDeAyuda) |
1 | /** |
2 | * Detalle de los errores devueltos por un servicio. |
3 | */ |
4 | export class ProblemDetails extends Error { |
5 | |
6 | /** |
7 | * @param {number} status |
8 | * @param {Headers} headers |
9 | * @param {string} title |
10 | * @param {string} [type] |
11 | * @param {string} [detail] |
12 | */ |
13 | constructor(status, headers, title, type, detail) { |
14 | super(title) |
15 | /** |
16 | * @readonly |
17 | */ |
18 | this.status = status |
19 | /** |
20 | * @readonly |
21 | */ |
22 | this.headers = headers |
23 | /** |
24 | * @readonly |
25 | */ |
26 | this.type = type |
27 | /** |
28 | * @readonly |
29 | */ |
30 | this.detail = detail |
31 | /** |
32 | * @readonly |
33 | */ |
34 | this.title = title |
35 | } |
36 | |
37 | } |
1 | /** |
2 | * @template { HTMLElement } T |
3 | * @param { Document | Element | ShadowRoot } raiz |
4 | * @param { string } query |
5 | * @returns { T } |
6 | */ |
7 | export function querySelector(raiz, query) { |
8 | /** @type { T | null } */ |
9 | const resutado = raiz.querySelector(query) |
10 | if (resutado === null) |
11 | throw new Error(`No se encuentra ${query}.`) |
12 | return resutado |
13 | } |
1 | import { querySelector } from "./querySelector.js" |
2 | |
3 | /** |
4 | * @param {string[]} paginas |
5 | */ |
6 | export function resaltaSiEstasEn(paginas) { |
7 | |
8 | const pathname = location.pathname |
9 | |
10 | for (const pagina of paginas) { |
11 | |
12 | if (pathname === pagina) { |
13 | setTimeout(() => { |
14 | const tab = document.querySelector(".active") |
15 | if (tab !== null && tab.closest(".scrollable") !== null) { |
16 | tab.scrollIntoView({ inline: "center", block: "end" }) |
17 | } |
18 | }) |
19 | return `class="active"` |
20 | } |
21 | |
22 | } |
23 | |
24 | return "" |
25 | |
26 | } |
1 | export const ES_APPLE = /.*(iPad|iPhone|iPod|Mac).*/.test(navigator.userAgent) |
1 | import { MdNavigationDrawer } from "./MdNavigationDrawer.js" |
2 | |
3 | export class MdMenuButton extends HTMLButtonElement { |
4 | |
5 | constructor() { |
6 | super() |
7 | this.abreDrawer = this.abreDrawer.bind(this) |
8 | } |
9 | |
10 | connectedCallback() { |
11 | this.type = "button" |
12 | this.classList.add("md-standard-icon-button") |
13 | this.innerHTML = /* HTML */ |
14 | `<span class="material-symbols-outlined">menu</span>` |
15 | this.addEventListener("click", this.abreDrawer) |
16 | } |
17 | |
18 | disconnectedCallback() { |
19 | this.removeEventListener("click", this.abreDrawer) |
20 | } |
21 | |
22 | abreDrawer() { |
23 | const drawer = document.querySelector(".drawer") |
24 | if (drawer instanceof MdNavigationDrawer) { |
25 | drawer.abre() |
26 | } |
27 | } |
28 | } |
29 | |
30 | customElements.define("md-menu-button", MdMenuButton, { extends: "button" }) |
1 | import { abreElementoHtml } from "../abreElementoHtml.js" |
2 | import { cierraElementoHtmo } from "../cierraElementoHtmo.js" |
3 | import { querySelector } from "../querySelector.js" |
4 | |
5 | export class MdOptionsMenu extends HTMLElement { |
6 | |
7 | getContent() { |
8 | return /* HTML */` |
9 | |
10 | <style> |
11 | |
12 | :host { |
13 | position: absolute; |
14 | } |
15 | |
16 | </style> |
17 | |
18 | <slot></slot>` |
19 | } |
20 | |
21 | constructor() { |
22 | super() |
23 | const shadow = this.attachShadow({ mode: "open" }) |
24 | shadow.innerHTML = this.getContent() |
25 | this._configuraOpciones = this._configuraOpciones.bind(this) |
26 | |
27 | /** |
28 | * @private |
29 | * @type { HTMLSlotElement } |
30 | */ |
31 | this._slot = querySelector(shadow, "slot") |
32 | /** |
33 | * @private |
34 | * @type { HTMLElement[] } |
35 | */ |
36 | this._opciones = [] |
37 | this._slot.addEventListener("slotchange", this._configuraOpciones) |
38 | } |
39 | |
40 | connectedCallback() { |
41 | this.classList.add("md-menu") |
42 | this.role = "listbox" |
43 | } |
44 | |
45 | /** |
46 | * @returns {readonly Readonly<HTMLElement>[]} |
47 | */ |
48 | get opciones() { |
49 | return this._opciones |
50 | } |
51 | |
52 | get seleccion() { |
53 | /** @type { HTMLInputElement | null } */ |
54 | const seleccionado = this.querySelector(".selected") |
55 | return seleccionado === null ? "" : seleccionado.value |
56 | } |
57 | |
58 | _configuraOpciones() { |
59 | /** |
60 | * @type {HTMLElement[]} |
61 | */ |
62 | const opciones = [] |
63 | for (const opcion of this._slot.assignedElements()) { |
64 | opcion.role = "option" |
65 | if (opcion instanceof HTMLElement) { |
66 | opciones.push(opcion) |
67 | } |
68 | } |
69 | this._opciones = opciones |
70 | } |
71 | |
72 | abre() { |
73 | abreElementoHtml(this) |
74 | } |
75 | |
76 | |
77 | cierra() { |
78 | cierraElementoHtmo(this) |
79 | } |
80 | |
81 | /** |
82 | * @param {string} value |
83 | */ |
84 | muestraValue(value) { |
85 | let texto = "" |
86 | for (const opcion of this._opciones) { |
87 | if (opcion.dataset.value === value) { |
88 | opcion.classList.add("selected") |
89 | let textContent = opcion.textContent |
90 | if (texto === "" && textContent !== null) { |
91 | textContent = textContent.trim() |
92 | if (textContent !== "") { |
93 | texto = textContent |
94 | } |
95 | } |
96 | } else { |
97 | opcion.classList.remove("selected") |
98 | } |
99 | } |
100 | return texto |
101 | } |
102 | |
103 | } |
104 | |
105 | customElements.define("md-options-menu", MdOptionsMenu) |
1 | import { ES_APPLE } from "../const/ES_APPLE.js" |
2 | |
3 | export class MdOverflowButton extends HTMLButtonElement { |
4 | |
5 | connectedCallback() { |
6 | this.type = "button" |
7 | this.classList.add("md-standard-icon-button") |
8 | this.innerHTML = ES_APPLE |
9 | ? /* HTML */ |
10 | `<span style="color: var(--md-sys-color-on-surface-variant)" |
11 | class="material-symbols-outlined"> |
12 | more_horiz |
13 | </span>` |
14 | : /* HTML */ |
15 | `<span style="color: var(--md-sys-color-on-surface-variant)" |
16 | class="material-symbols-outlined"> |
17 | more_vert |
18 | </span>` |
19 | } |
20 | |
21 | } |
22 | |
23 | customElements |
24 | .define("md-overflow-button", MdOverflowButton, { extends: "button" }) |
1 | import { abreElementoHtml } from "../abreElementoHtml.js" |
2 | import { cierraElementoHtmo } from "../cierraElementoHtmo.js" |
3 | |
4 | export class MdOverflowMenu extends HTMLElement { |
5 | |
6 | getContent() { |
7 | return /* HTML */` |
8 | |
9 | <style> |
10 | |
11 | :host { |
12 | position: fixed; |
13 | min-width: 7rem; |
14 | max-width: 280px; |
15 | } |
16 | |
17 | ::slotted(*) { |
18 | text-align: start; |
19 | width: 100%; |
20 | border: none; |
21 | background-color: transparent; |
22 | } |
23 | |
24 | </style> |
25 | |
26 | <slot></slot>` |
27 | } |
28 | |
29 | constructor() { |
30 | super() |
31 | const shadow = this.attachShadow({ mode: "open" }) |
32 | shadow.innerHTML = this.getContent() |
33 | this.clicCierra = this.clicCierra.bind(this) |
34 | /** |
35 | * @private |
36 | * @type {HTMLButtonElement| null} |
37 | */ |
38 | this._toggleButton = null |
39 | } |
40 | |
41 | connectedCallback() { |
42 | this.classList.add("md-menu") |
43 | this.role = "menu" |
44 | } |
45 | |
46 | /** |
47 | * @param {HTMLButtonElement} toggleButton |
48 | */ |
49 | alterna(toggleButton) { |
50 | this._toggleButton = toggleButton |
51 | const top = toggleButton.offsetTop + toggleButton.offsetHeight - 4 |
52 | const right = |
53 | innerWidth - (toggleButton.offsetLeft + toggleButton.offsetWidth) - 3 |
54 | this.style.top = `${top}px` |
55 | this.style.right = `${right}px` |
56 | const list = this.classList |
57 | if (list.contains("open")) { |
58 | this.cierra() |
59 | } else { |
60 | this.abre() |
61 | } |
62 | } |
63 | |
64 | abre() { |
65 | document.addEventListener("click", this.clicCierra) |
66 | abreElementoHtml(this) |
67 | } |
68 | |
69 | cierra() { |
70 | document.removeEventListener("click", this.clicCierra) |
71 | cierraElementoHtmo(this) |
72 | } |
73 | |
74 | /** |
75 | * @param {Event} evt |
76 | */ |
77 | clicCierra(evt) { |
78 | const target = evt.target |
79 | if (this.classList.contains("open") |
80 | && this._toggleButton !== null |
81 | && target instanceof HTMLElement |
82 | && !this._toggleButton.contains(target)) { |
83 | this.cierra() |
84 | } |
85 | } |
86 | } |
87 | |
88 | customElements.define("md-overflow-menu", MdOverflowMenu) |
1 | import { getAttribute } from "../getAttribute.js" |
2 | import { querySelector } from "../querySelector.js" |
3 | import { MdOptionsMenu } from "./md-options-menu.js" |
4 | |
5 | export class MdSelectMenu extends HTMLElement { |
6 | |
7 | static get observedAttributes() { |
8 | return ["options", "value", "required"] |
9 | } |
10 | |
11 | getContent() { |
12 | return /* HTML */ ` |
13 | <link rel="stylesheet" href="/lib/css/material-symbols-outlined.css"> |
14 | |
15 | <style> |
16 | :host { |
17 | display: block; |
18 | cursor: default; |
19 | } |
20 | |
21 | output { |
22 | display: block; |
23 | padding-right: 2rem; |
24 | white-space: nowrap; |
25 | text-overflow: ellipsis; |
26 | overflow: hidden; |
27 | } |
28 | |
29 | #up { |
30 | position: absolute; |
31 | bottom: 0.5rem; |
32 | right: 0.75rem; |
33 | display: none; |
34 | color: var(--md-sys-color-on-surface-variant); |
35 | } |
36 | |
37 | #down { |
38 | position: absolute; |
39 | bottom: 0.5rem; |
40 | right: 0.75rem; |
41 | color: var(--md-sys-color-on-surface-variant); |
42 | } |
43 | |
44 | :host(.open) #up { |
45 | display: inline-block; |
46 | } |
47 | |
48 | :host(.open) #down { |
49 | display: none; |
50 | } |
51 | |
52 | :host(:invalid) #up, |
53 | :host(:invalid) #down { |
54 | color: var(--md-sys-color-error); |
55 | } |
56 | |
57 | </style> |
58 | <output></output> |
59 | <span id="down" class="material-symbols-outlined"> |
60 | arrow_drop_down |
61 | </span> |
62 | <span id="up" class="material-symbols-outlined"> |
63 | arrow_drop_up |
64 | </span>` |
65 | } |
66 | |
67 | constructor() { |
68 | super() |
69 | |
70 | const shadow = this.attachShadow({ mode: "open" }) |
71 | shadow.innerHTML = this.getContent() |
72 | |
73 | this._alterna = this._alterna.bind(this) |
74 | this._onKeyDown = this._onKeyDown.bind(this) |
75 | this._cierra = this._cierra.bind(this) |
76 | this._clicEnDialogo = this._clicEnDialogo.bind(this) |
77 | this.clicExterno = this.clicExterno.bind(this) |
78 | this.muestraValue = this.muestraValue.bind(this) |
79 | |
80 | /** |
81 | * @private |
82 | * @type {string} |
83 | */ |
84 | this._customValidity = "" |
85 | |
86 | /** |
87 | * @private |
88 | * @type { HTMLOutputElement } |
89 | */ |
90 | this.output = querySelector(shadow, "output") |
91 | /** |
92 | * @private |
93 | * @type { MdOptionsMenu | null } |
94 | */ |
95 | this._optionsMenu = null |
96 | /** |
97 | * @protected |
98 | * @readonly |
99 | */ |
100 | this._internals = this.attachInternals() |
101 | this._internals.role = "select" |
102 | addEventListener("load", this.muestraValue) |
103 | } |
104 | |
105 | connectedCallback() { |
106 | this.tabIndex = 0 |
107 | this.role = "combobox" |
108 | this.ariaHasPopup = "listbox" |
109 | this.ariaExpanded = "false" |
110 | this["aria-controls"] = this.options |
111 | this.addEventListener("keydown", this._onKeyDown) |
112 | const parentElement = this.parentElement |
113 | if (parentElement !== null) { |
114 | parentElement.addEventListener("click", this._alterna) |
115 | } |
116 | } |
117 | |
118 | /** |
119 | * @param {string} nombreDeAtributo |
120 | * @param {string} _valorAnterior |
121 | * @param {string} _nuevoValor |
122 | */ |
123 | attributeChangedCallback(nombreDeAtributo, _valorAnterior, _nuevoValor) { |
124 | switch (nombreDeAtributo) { |
125 | case "options": |
126 | this._cambiaOptions() |
127 | break |
128 | case "value": |
129 | this.muestraValue() |
130 | break |
131 | case "required": |
132 | this.checkValidity() |
133 | break |
134 | } |
135 | } |
136 | |
137 | get options() { |
138 | return getAttribute(this, "options") |
139 | } |
140 | |
141 | set options(options) { |
142 | this.setAttribute("options", options) |
143 | } |
144 | |
145 | _cambiaOptions() { |
146 | if (this._optionsMenu !== null) { |
147 | this._optionsMenu = null |
148 | } |
149 | this["aria-controls"] = this.options |
150 | } |
151 | |
152 | get required() { |
153 | return this.hasAttribute("required") |
154 | } |
155 | |
156 | set required(required) { |
157 | this.toggleAttribute("required", Boolean(required)) |
158 | } |
159 | |
160 | get value() { |
161 | return getAttribute(this, "value") |
162 | } |
163 | |
164 | set value(value) { |
165 | this.setAttribute("value", value) |
166 | } |
167 | |
168 | get name() { |
169 | return getAttribute(this, "name") |
170 | } |
171 | |
172 | set name(name) { |
173 | this.setAttribute("name", name) |
174 | } |
175 | |
176 | muestraValue() { |
177 | const value = this.value |
178 | this._internals.setFormValue(value) |
179 | |
180 | // En un futuro se usará esto en vez de la clase populated. |
181 | // if (value === "") { |
182 | // this._internals.states.delete("populated") |
183 | // } else { |
184 | // this._internals.states.add("populated") |
185 | // } |
186 | |
187 | if (this.isConnected) { |
188 | if (value === "") { |
189 | this.classList.remove("populated") |
190 | } else { |
191 | this.classList.add("populated") |
192 | } |
193 | this._checkValidity() |
194 | const optionsMenu = this.optionsMenu |
195 | if (optionsMenu !== null) { |
196 | this.output.value = optionsMenu.muestraValue(value) |
197 | } |
198 | } |
199 | } |
200 | |
201 | get form() { |
202 | return this._internals && this._internals.form |
203 | } |
204 | |
205 | get willValidate() { |
206 | return this._internals ? this._internals.willValidate : true |
207 | } |
208 | |
209 | /** |
210 | * @param {string} message |
211 | */ |
212 | setCustomValidity(message) { |
213 | this._customValidity = message |
214 | this._checkValidity() |
215 | } |
216 | |
217 | /** |
218 | * @returns {ValidityState} |
219 | */ |
220 | get validity() { |
221 | return this._internals.validity |
222 | } |
223 | |
224 | checkValidity() { |
225 | return this._internals.checkValidity() |
226 | } |
227 | |
228 | reportValidity() { |
229 | return this._internals.reportValidity() |
230 | } |
231 | |
232 | get validationMessage() { |
233 | return this._internals.validationMessage |
234 | } |
235 | /** @returns {boolean} */ |
236 | _checkValidity() { |
237 | if (this._customValidity !== "") { |
238 | this._internals.setValidity({ customError: true }, this._customValidity) |
239 | return false |
240 | } else if (this.required && this.value === "") { |
241 | this._internals.setValidity({ valueMissing: true }, "Seleccione una opción.") |
242 | return false |
243 | } else { |
244 | this._internals.setValidity({}) |
245 | return true |
246 | } |
247 | } |
248 | |
249 | /** @private */ |
250 | _alterna() { |
251 | if (this.classList.contains("open")) { |
252 | this._cierra() |
253 | } else { |
254 | this._abre() |
255 | } |
256 | } |
257 | |
258 | /** @private */ |
259 | _abre() { |
260 | this.classList.add("open") |
261 | const parentElement = this.parentElement |
262 | if (parentElement !== null) { |
263 | const optionsMenu = this.optionsMenu |
264 | if (optionsMenu !== null) { |
265 | optionsMenu.style.top = `${parentElement.offsetTop + 58}px` |
266 | optionsMenu.style.left = `${parentElement.offsetLeft}px` |
267 | optionsMenu.style.width = `${parentElement.offsetWidth}px` |
268 | optionsMenu.abre() |
269 | this.focus() |
270 | optionsMenu.addEventListener("click", this._clicEnDialogo) |
271 | } |
272 | this.ariaExpanded = "true" |
273 | document.addEventListener("click", this.clicExterno) |
274 | } |
275 | } |
276 | |
277 | /** @private */ |
278 | _cierra() { |
279 | this.classList.remove("open") |
280 | const optionsMenu = this.optionsMenu |
281 | if (optionsMenu !== null) { |
282 | optionsMenu.cierra() |
283 | optionsMenu.removeEventListener("click", this._clicEnDialogo) |
284 | } |
285 | this.ariaExpanded = "false" |
286 | document.removeEventListener("click", this.clicExterno) |
287 | this.dispatchEvent(new Event("input", { bubbles: true })) |
288 | } |
289 | |
290 | get optionsMenu() { |
291 | if (this._optionsMenu === null) { |
292 | if (this.options !== "") { |
293 | const optionsMenu = document.getElementById(this.options) |
294 | if (optionsMenu instanceof MdOptionsMenu) { |
295 | this._optionsMenu = optionsMenu |
296 | } else { |
297 | throw new Error(`Valor incorrecto para options: "${this.options}".`) |
298 | } |
299 | } |
300 | } |
301 | return this._optionsMenu |
302 | } |
303 | |
304 | /** @private */ |
305 | _avanzaOpcion() { |
306 | const i = this._valueIndex |
307 | if (i > -1) { |
308 | const optionsMenu = this.optionsMenu |
309 | if (optionsMenu !== null) { |
310 | const opciones = optionsMenu.opciones |
311 | if (i < opciones.length - 1) { |
312 | this.value = getAttribute(opciones[i + 1], "data-value") |
313 | } |
314 | } |
315 | } |
316 | } |
317 | |
318 | /** @private */ |
319 | _retrocedeOpcion() { |
320 | const i = this._valueIndex |
321 | if (i > -1) { |
322 | const optionsMenu = this.optionsMenu |
323 | if (optionsMenu !== null) { |
324 | const opciones = optionsMenu.opciones |
325 | if (i > 0) { |
326 | this.value = getAttribute(opciones[i - 1], "data-value") |
327 | } |
328 | } |
329 | } |
330 | } |
331 | |
332 | /** |
333 | * @private |
334 | * @returns {number} |
335 | */ |
336 | get _valueIndex() { |
337 | const value = this.value |
338 | const optionsMenu = this.optionsMenu |
339 | return (optionsMenu === null |
340 | ? -1 |
341 | : optionsMenu.opciones.findIndex(opcion => opcion.dataset.value === value)) |
342 | } |
343 | |
344 | /** |
345 | * @private |
346 | * @param {Event} event |
347 | */ |
348 | _clicEnDialogo(event) { |
349 | const target = event.target |
350 | const optionsMenu = this.optionsMenu |
351 | let value = "" |
352 | if (optionsMenu !== null) { |
353 | for (const opcion of optionsMenu.opciones) { |
354 | if (opcion === target) { |
355 | opcion.classList.add("selected") |
356 | value = getAttribute(opcion, "data-value") |
357 | } else { |
358 | opcion.classList.remove("selected") |
359 | } |
360 | } |
361 | } |
362 | this.value = value |
363 | this._cierra() |
364 | this.focus() |
365 | } |
366 | |
367 | /** |
368 | * @param {Event} evt |
369 | */ |
370 | clicExterno(evt) { |
371 | const target = evt.target |
372 | const parentElement = this.parentElement |
373 | const optionsMenu = this._optionsMenu |
374 | if (this.classList.contains("open") |
375 | && target instanceof HTMLElement |
376 | && parentElement !== null |
377 | && !parentElement.contains(target) |
378 | && optionsMenu !== null |
379 | && !optionsMenu.contains(target)) { |
380 | this._cierra() |
381 | } |
382 | } |
383 | |
384 | /** |
385 | * @param { KeyboardEvent } event |
386 | */ |
387 | _onKeyDown(event) { |
388 | const key = event.key |
389 | const optionsMenu = this._optionsMenu |
390 | if (optionsMenu !== null) { |
391 | if (optionsMenu.classList.contains("open")) { |
392 | if (key === "ArrowDown") { |
393 | event.preventDefault() |
394 | this._avanzaOpcion() |
395 | } else if (key === "ArrowUp") { |
396 | event.preventDefault() |
397 | this._retrocedeOpcion() |
398 | } else if (key === "Escape") { |
399 | event.preventDefault() |
400 | this._cierra() |
401 | } else if (key === " ") { |
402 | event.preventDefault() |
403 | this._cierra() |
404 | } else if (key === "Tab") { |
405 | this._cierra() |
406 | } else { |
407 | event.preventDefault() |
408 | } |
409 | } else if (key === " ") { |
410 | event.preventDefault() |
411 | this._abre() |
412 | } else if (key === "Tab") { |
413 | this._cierra() |
414 | } else { |
415 | event.preventDefault() |
416 | } |
417 | } |
418 | } |
419 | |
420 | } |
421 | |
422 | MdSelectMenu.formAssociated = true |
423 | |
424 | customElements.define("md-select-menu", MdSelectMenu) |
1 | import { querySelector } from "../querySelector.js" |
2 | |
3 | export class MdSliderField extends HTMLElement { |
4 | |
5 | getContent() { |
6 | return /* HTML */` |
7 | <style> |
8 | :host { |
9 | display: block; |
10 | margin: 1rem; |
11 | } |
12 | |
13 | :host([hidden]) { |
14 | display: none; |
15 | } |
16 | |
17 | #label::slotted(*) { |
18 | display: block; |
19 | white-space: nowrap; |
20 | text-overflow: ellipsis; |
21 | overflow: hidden; |
22 | color: var(--md-sys-color-on-surface-variant); |
23 | font-family: var(--md-sys-typescale-body-small-font); |
24 | font-weight: var(--md-sys-typescale-body-small-weight); |
25 | font-size: var(--md-sys-typescale-body-small-size); |
26 | font-style: var(--md-sys-typescale-body-small-font-style); |
27 | letter-spacing: var(--md-sys-typescale-body-small-tracking); |
28 | line-height: var(--md-sys-typescale-body-small-line-height); |
29 | text-transform: var(--md-sys-typescale-body-small-text-transform); |
30 | text-decoration: var(--md-sys-typescale-body-small-text-decoration); |
31 | } |
32 | |
33 | [name="slider"]::slotted(input) { |
34 | -webkit-appearance: none; |
35 | appearance: none; |
36 | height: 0.25rem; |
37 | border-radius: 0.125rem; |
38 | background-image: |
39 | linear-gradient(to right, var(--md-sys-color-primary) 0%, var(--md-sys-color-primary) 50%, var(--md-sys-color-surface-container-highest) 50%, var(--md-sys-color-surface-container-highest) 100%); |
40 | } |
41 | |
42 | [name="slider"]::slotted(input:focus) { |
43 | outline: none; |
44 | } |
45 | |
46 | [name="supporting"]::slotted(*) { |
47 | display: block; |
48 | color: var(--md-sys-color-on-surface-variant); |
49 | font-family: var(--md-sys-typescale-body-small-font); |
50 | font-weight: var(--md-sys-typescale-body-small-weight); |
51 | font-size: var(--md-sys-typescale-body-small-size); |
52 | font-style: var(--md-sys-typescale-body-small-font-style); |
53 | letter-spacing: var(--md-sys-typescale-body-small-tracking); |
54 | line-height: var(--md-sys-typescale-body-small-line-height); |
55 | text-transform: var(--md-sys-typescale-body-small-text-transform); |
56 | text-decoration: var(--md-sys-typescale-body-small-text-decoration); |
57 | padding-top: 0.5rem; |
58 | white-space: nowrap; |
59 | text-overflow: ellipsis; |
60 | overflow: hidden; |
61 | } |
62 | </style> |
63 | <slot id="label"></slot> |
64 | <slot name="slider"></slot> |
65 | <slot name="supporting"></slot>` |
66 | } |
67 | |
68 | constructor() { |
69 | super() |
70 | const shadow = this.attachShadow({ mode: "open", delegatesFocus: true }) |
71 | shadow.innerHTML = this.getContent() |
72 | this._configuraSlider = this._configuraSlider.bind(this) |
73 | this.analiza = this.analiza.bind(this) |
74 | |
75 | /** |
76 | * @private |
77 | * @type {HTMLSlotElement} |
78 | */ |
79 | this._slotSlider = querySelector(shadow, '[name="slider"]') |
80 | /** |
81 | * @private |
82 | * @type {HTMLInputElement|null} |
83 | */ |
84 | this._input = null |
85 | this._slotSlider.addEventListener("slotchange", this._configuraSlider) |
86 | } |
87 | |
88 | /** @private */ |
89 | _configuraSlider() { |
90 | if (this._input !== null) { |
91 | this._input.removeEventListener("input", this.analiza) |
92 | this._input = null |
93 | } |
94 | for (const input of this._slotSlider.assignedElements()) { |
95 | if (input instanceof HTMLInputElement) { |
96 | this._input = input |
97 | input.addEventListener("input", this.analiza) |
98 | this.analiza() |
99 | } |
100 | } |
101 | } |
102 | |
103 | analiza() { |
104 | const i = this._input |
105 | if (i !== null) { |
106 | const v = i.valueAsNumber |
107 | const min = parseFloat(i.min) |
108 | const max = parseFloat(i.max) |
109 | const value = (v - min) / (max - min) * 100 |
110 | i.title = v.toString() |
111 | i.style.background = |
112 | `linear-gradient(to right, var(--md-sys-color-primary) 0%, var(--md-sys-color-primary) ${value |
113 | }%, var(--md-sys-color-surface-container-highest) ${value |
114 | }%, var(--md-sys-color-surface-container-highest) 100%)` |
115 | } |
116 | } |
117 | |
118 | } |
119 | |
120 | customElements.define("md-slider-field", MdSliderField) |
1 | import { ES_APPLE } from "../const/ES_APPLE.js" |
2 | import { getAttribute } from "../getAttribute.js" |
3 | import { querySelector } from "../querySelector.js" |
4 | |
5 | class MdTopAppBar extends HTMLElement { |
6 | |
7 | getContent() { |
8 | return /* HTML */` |
9 | <style> |
10 | |
11 | :host { |
12 | display: flex; |
13 | box-sizing: border-box; |
14 | align-items: center; |
15 | padding: 0 0.25rem; |
16 | background-color: var(--md-sys-color-surface); |
17 | position: sticky; |
18 | z-index: 1; |
19 | left: env(titlebar-area-x, 0); |
20 | top: env(titlebar-area-y, 0); |
21 | height: env(titlebar-area-height, 4rem); |
22 | width: env(titlebar-area-width, 100%); |
23 | } |
24 | |
25 | :host(.apple) { |
26 | height: env(titlebar-area-height, 3rem); |
27 | } |
28 | |
29 | :host(.scroll) { |
30 | background-color: var(--md-sys-color-surface-container-low); |
31 | } |
32 | |
33 | #navigation { |
34 | flex: 0 0 auto; |
35 | overflow: hidden |
36 | } |
37 | |
38 | #navigation ::slotted(*) { |
39 | color: var(--md-sys-color-on-surface); |
40 | } |
41 | |
42 | #acciones { |
43 | margin-left: auto; |
44 | flex: 0 0 auto; |
45 | overflow: hidden |
46 | } |
47 | |
48 | :host(.centrado) #acciones, |
49 | :host(.center-aligned) #acciones { |
50 | flex: 0 0 3rem; |
51 | overflow: hidden |
52 | } |
53 | |
54 | #headline::slotted(*) { |
55 | -webkit-app-region: drag; |
56 | flex: 1 1 auto; |
57 | white-space: nowrap; |
58 | text-overflow: ellipsis; |
59 | overflow: hidden; |
60 | font-family: var(--md-sys-typescale-title-large-font); |
61 | font-weight: var(--md-sys-typescale-title-large-weight); |
62 | font-size: var(--md-sys-typescale-title-large-size); |
63 | font-style: var(--md-sys-typescale-title-large-font-style); |
64 | letter-spacing: var(--md-sys-typescale-title-large-tracking); |
65 | line-height: var(--md-sys-typescale-title-large-line-height); |
66 | text-transform: var(--md-sys-typescale-title-large-text-transform); |
67 | text-decoration: var(--md-sys-typescale-title-large-text-decoration); |
68 | color: var(--md-sys-color-on-surface); |
69 | } |
70 | |
71 | :host(.center-aligned) #headline::slotted(*) { |
72 | flex: 1 1 auto; |
73 | text-align: center |
74 | } |
75 | |
76 | </style> |
77 | |
78 | <span id="navigation"> |
79 | <slot name="navigation"></slot> |
80 | </span> |
81 | <slot id="headline"></slot> |
82 | <span id="acciones"> |
83 | <slot name="action"></slot> |
84 | </span>` |
85 | } |
86 | |
87 | constructor() { |
88 | super() |
89 | if (ES_APPLE) { |
90 | document.body.classList.add("apple") |
91 | document.body.classList.remove("material") |
92 | } else { |
93 | document.body.classList.add("material") |
94 | document.body.classList.remove("apple") |
95 | } |
96 | |
97 | /** |
98 | * @private |
99 | * @readonly |
100 | */ |
101 | const shadow = this.attachShadow({ mode: "open" }) |
102 | shadow.innerHTML = this.getContent() |
103 | this._configuraAction = this._configuraAction.bind(this) |
104 | /** |
105 | * @private |
106 | * @type {number} |
107 | */ |
108 | this._posY = 0 |
109 | /** |
110 | * @private |
111 | * @type {boolean} |
112 | */ |
113 | this._scrolling = false |
114 | /** |
115 | * @private |
116 | * @type { HTMLSlotElement } |
117 | */ |
118 | this._navigation = querySelector(shadow, '[name="navigation"]') |
119 | /** |
120 | * @private |
121 | * @type { HTMLSlotElement } |
122 | */ |
123 | this._action = querySelector(shadow, '[name="action"]') |
124 | /** |
125 | * @private |
126 | * @type { HTMLHeadingElement | null } |
127 | */ |
128 | this._headline = null |
129 | /** |
130 | * @private |
131 | * @type { HTMLElement | null } |
132 | */ |
133 | this._adicional = null |
134 | this._action.addEventListener("slotchange", this._configuraAction) |
135 | addEventListener("scroll", () => this._onScroll()) |
136 | addEventListener("load", () => this.configurOtros()) |
137 | } |
138 | |
139 | connectedCallback() { |
140 | this.role = "toolbar" |
141 | this._configuraAction() |
142 | } |
143 | |
144 | configurOtros() { |
145 | const idHeadline = getAttribute(this, "headline") |
146 | if (idHeadline !== "") { |
147 | const headline = document.getElementById(idHeadline) |
148 | if (headline instanceof HTMLHeadingElement) { |
149 | this._headline = headline |
150 | if (this.classList.contains("apple") || this.classList.contains("medium")) { |
151 | headline.classList.add("md-headline", "headline-small") |
152 | } else { |
153 | headline.classList.add("md-headline", "headline-medium") |
154 | } |
155 | } |
156 | } |
157 | const idAdicional = getAttribute(this, "adicional") |
158 | if (idAdicional !== "") { |
159 | this._adicional = document.getElementById(idAdicional) |
160 | if (this._adicional !== null) { |
161 | if (this.classList.contains("apple")) { |
162 | this._adicional.style.top = "env(titlebar-area-height, 3rem)" |
163 | } else { |
164 | this._adicional.style.top = "env(titlebar-area-height, 4rem)" |
165 | } |
166 | } |
167 | } |
168 | } |
169 | |
170 | _configuraAction() { |
171 | const assignedElements = this._action.assignedElements() |
172 | if (this.isConnected) { |
173 | if (ES_APPLE) { |
174 | this.classList.add("apple") |
175 | this.classList.remove("material") |
176 | } else { |
177 | this.classList.add("material") |
178 | this.classList.remove("apple") |
179 | } |
180 | if (this.classList.contains("center-aligned")) { |
181 | this.classList.remove("centrado") |
182 | this.classList.remove("justificado") |
183 | } else { |
184 | if (ES_APPLE && assignedElements.length <= 1) { |
185 | this.classList.add("centrado") |
186 | this.classList.remove("justificado") |
187 | } else { |
188 | this.classList.add("justificado") |
189 | this.classList.remove("centrado") |
190 | } |
191 | } |
192 | } |
193 | } |
194 | |
195 | /** @private */ |
196 | _onScroll() { |
197 | this._posY = scrollY |
198 | if (!this._scrolling) { |
199 | requestAnimationFrame(() => this._avanza()) |
200 | } |
201 | this._scrolling = true |
202 | } |
203 | |
204 | /** @private */ |
205 | _avanza() { |
206 | if (this._posY === 0) { |
207 | this.classList.remove("scroll") |
208 | if (this._headline !== null) { |
209 | if (this._adicional === null) { |
210 | this._headline.classList.remove("scroll") |
211 | } else { |
212 | this._headline.classList.remove("scroll-adicional") |
213 | } |
214 | } |
215 | if (this._adicional !== null) { |
216 | this._adicional.classList.remove("scroll") |
217 | } |
218 | } else { |
219 | this.classList.add("scroll") |
220 | if (this._headline !== null) { |
221 | if (this._adicional === null) { |
222 | this._headline.classList.add("scroll") |
223 | } else { |
224 | this._headline.classList.add("scroll-adicional") |
225 | } |
226 | } |
227 | if (this._adicional !== null) { |
228 | this._adicional.classList.add("scroll") |
229 | } |
230 | } |
231 | this._scrolling = false |
232 | } |
233 | |
234 | } |
235 | |
236 | customElements.define("md-top-app-bar", MdTopAppBar) |
1 | import { abreElementoHtml } from "../abreElementoHtml.js" |
2 | import { cierraElementoHtmo } from "../cierraElementoHtmo.js" |
3 | import { querySelector } from "../querySelector.js" |
4 | |
5 | export class MdNavigationDrawer extends HTMLElement { |
6 | |
7 | /** |
8 | * @returns {string} |
9 | */ |
10 | getHipervinculos() { throw new Error("abstract") } |
11 | |
12 | getContent() { |
13 | return /* HTML */` |
14 | |
15 | <link rel="stylesheet" href="/lib/css/material-symbols-outlined.css"> |
16 | <link rel="stylesheet" href="/lib/css/md-ripple.css"> |
17 | <link rel="stylesheet" href="/material-tokens/css/shape.css"> |
18 | <link rel="stylesheet" href="/material-tokens/css/motion.css"> |
19 | |
20 | <style> |
21 | |
22 | :host { |
23 | display: block; |
24 | } |
25 | |
26 | :host([hidden]) { |
27 | display: none; |
28 | } |
29 | |
30 | nav { |
31 | display: none; |
32 | flex-direction: column; |
33 | position: fixed; |
34 | z-index: 4; |
35 | box-sizing: border-box; |
36 | top: 0; |
37 | left: 0; |
38 | bottom: 0; |
39 | width: var(--anchoNav); |
40 | max-width: 80vw; |
41 | overflow: hidden; |
42 | overscroll-behavior: contain; |
43 | background-color: var(--md-sys-color-surface-container-low); |
44 | transform: translateX(-100%); |
45 | transition-property: display, transform; |
46 | transition-behavior: allow-discrete; |
47 | } |
48 | |
49 | nav.open { |
50 | display: flex; |
51 | transform: translateX(0); |
52 | } |
53 | |
54 | nav>div { |
55 | flex-grow: 1; |
56 | overflow: auto; |
57 | padding: 0.75rem 1rem; |
58 | } |
59 | |
60 | h1 { |
61 | margin: 0; |
62 | height: 3.5rem; |
63 | line-height: 3.5rem; |
64 | padding: 0 0 0 0.75rem; |
65 | white-space: nowrap; |
66 | text-overflow: ellipsis; |
67 | overflow: hidden; |
68 | color: var(--md-sys-color-on-surface-variant); |
69 | font-family: var(--md-sys-typescale-title-small-font); |
70 | font-weight: var(--md-sys-typescale-title-small-weight); |
71 | font-size: var(--md-sys-typescale-title-small-size); |
72 | font-style: var(--md-sys-typescale-title-small-font-style); |
73 | letter-spacing: var(--md-sys-typescale-title-small-tracking); |
74 | text-transform: var(--md-sys-typescale-title-small-text-transform); |
75 | text-decoration: var(--md-sys-typescale-title-small-text-decoration); |
76 | } |
77 | |
78 | a::after { /* container inactive */ |
79 | content: ""; |
80 | position: absolute; |
81 | z-index: -2; |
82 | top: 0; |
83 | right: 0; |
84 | left: 0; |
85 | bottom: 0; |
86 | } |
87 | |
88 | a.active::after { /* container */ |
89 | background-color: var(--md-sys-color-secondary-container); |
90 | } |
91 | |
92 | a { /* label, shape inactive */ |
93 | position: relative; |
94 | display: block; |
95 | box-sizing: border-box; |
96 | height: 3.5rem; |
97 | line-height: 3.5rem; |
98 | padding: 0 0.75rem; |
99 | border-radius: 1.75rem; |
100 | color: var(--md-sys-color-on-surface-variant); |
101 | font-family: var(--md-sys-typescale-label-large-font); |
102 | font-weight: var(--md-sys-typescale-label-large-weight); |
103 | font-size: var(--md-sys-typescale-label-large-size); |
104 | font-style: var(--md-sys-typescale-label-large-font-style); |
105 | letter-spacing: var(--md-sys-typescale-label-large-tracking); |
106 | text-transform: var(--md-sys-typescale-label-large-text-transform); |
107 | text-decoration: var(--md-sys-typescale-label-large-text-decoration); |
108 | overflow: hidden; |
109 | white-space: nowrap; |
110 | text-overflow: ellipsis; |
111 | } |
112 | |
113 | a.active { /* label, shape */ |
114 | font-weight: var(--md-sys-typescale-label-large-weight-prominent); |
115 | color: var(--md-sys-color-on-secondary-container); |
116 | } |
117 | |
118 | a::before { /* state layer */ |
119 | content: ""; |
120 | position: absolute; |
121 | z-index: -1; |
122 | top: 0; |
123 | right: 0; |
124 | left: 0; |
125 | bottom: 0; |
126 | } |
127 | |
128 | a span { /* inactive icon */ |
129 | position: relative; |
130 | margin-right: 0.75rem; |
131 | vertical-align: middle; |
132 | color: var(--md-sys-color-on-surface-variant); |
133 | font-size: 1.5rem; |
134 | width: 1.5rem; |
135 | height: 1.5rem; |
136 | } |
137 | |
138 | a.active span { /* icon */ |
139 | color: var(--md-sys-color-on-secondary-container); |
140 | } |
141 | |
142 | #scrim { |
143 | display: none; |
144 | position: fixed; |
145 | z-index: 3; |
146 | top: 0; |
147 | left: 0; |
148 | bottom: 0; |
149 | right: 0; |
150 | opacity: 0.4; |
151 | background-color: var(--md-ref-palette-neutral-variant20); |
152 | transform: translateX(-100%); |
153 | transition-property: display, transform; |
154 | transition-behavior: allow-discrete; |
155 | } |
156 | |
157 | #scrim.open { |
158 | display: block; |
159 | transform: translateX(0); |
160 | } |
161 | |
162 | @starting-style { |
163 | nav.open{ |
164 | display: flex; |
165 | transform: translateX(-100%); |
166 | } |
167 | #scrim.open { |
168 | display: block; |
169 | transform: translateX(-100%); |
170 | } |
171 | } |
172 | |
173 | a:hover { /* inactive label, shape */ |
174 | color: var(--md-sys-color-on-surface); |
175 | } |
176 | |
177 | a.active:hover { /* active label, shape */ |
178 | color: var(--md-sys-color-on-secondary-container); |
179 | } |
180 | |
181 | a:hover::before { /* inactive state layer */ |
182 | background-color: var(--md-sys-color-on-surface); |
183 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
184 | } |
185 | |
186 | a.active:hover::before { /* state layer */ |
187 | background-color: var(--md-sys-color-on-secondary-container); |
188 | } |
189 | |
190 | a:hover span { /* inactive icon */ |
191 | color: var(--md-sys-color-on-surface); |
192 | } |
193 | |
194 | a.active:hover span { /* icon */ |
195 | color: var(--md-sys-color-on-secondary-container); |
196 | } |
197 | |
198 | a:focus { /* inactive label, shape */ |
199 | outline: none; |
200 | color: var(--md-sys-color-on-surface); |
201 | } |
202 | |
203 | a.active:focus { /* label, shape */ |
204 | color: var(--md-sys-color-on-secondary-container); |
205 | } |
206 | |
207 | a:focus::before { /* inactive state layer */ |
208 | background-color: var(--md-sys-color-on-surface); |
209 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
210 | } |
211 | |
212 | a.active:focus::before { /* state layer */ |
213 | background-color: var(--md-sys-color-on-secondary-container); |
214 | } |
215 | |
216 | a:focus span { /* inactive icon */ |
217 | color: var(--md-sys-color-on-surface); |
218 | } |
219 | |
220 | a.active:focus span { /* icon */ |
221 | color: var(--md-sys-color-on-secondary-container); |
222 | } |
223 | |
224 | a:active { /* inactive pressed label, shape */ |
225 | background-position: center; |
226 | background-image: |
227 | radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%); |
228 | background-size: 100%; |
229 | animation-name: md-ripple; |
230 | animation-duration: var(--md-sys-motion-duration-500); |
231 | color: var(--md-sys-color-on-surface); |
232 | } |
233 | |
234 | a.active:active { /* active pressed label, shape */ |
235 | color: var(--md-sys-color-on-secondary-container); |
236 | } |
237 | |
238 | a:active::before { /* inactive pressed state layer */ |
239 | background-color: var(--md-sys-color-on-surface); |
240 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
241 | } |
242 | |
243 | a.active:active::before { /* active pressed state layer */ |
244 | background-color: var(--md-sys-color-on-secondary-container); |
245 | } |
246 | |
247 | a:active span { /* inactive pressed icon */ |
248 | color: var(--md-sys-color-on-surface); |
249 | } |
250 | |
251 | a.active:focus span { /* active pressed icon */ |
252 | color: var(--md-sys-color-on-secondary-container); |
253 | } |
254 | |
255 | </style> |
256 | |
257 | <div id="scrim"class="duration-700 easing-standard"></div> |
258 | <nav class="large-end duration-700 easing-standard"><div></div></nav>` |
259 | } |
260 | |
261 | constructor() { |
262 | super() |
263 | const shadow = this.attachShadow({ mode: "open", delegatesFocus: true }) |
264 | shadow.innerHTML = this.getContent() |
265 | this.cierra = this.cierra.bind(this) |
266 | |
267 | /** @type {HTMLElement} */ |
268 | this._nav = querySelector(shadow, "nav") |
269 | |
270 | /** @type {HTMLUListElement} */ |
271 | this._div = querySelector(this._nav, "div") |
272 | |
273 | /** @type {HTMLUListElement} */ |
274 | this._scrim = querySelector(shadow, "#scrim") |
275 | this._scrim.addEventListener("click", this.cierra) |
276 | } |
277 | |
278 | connectedCallback() { |
279 | this.classList.add("drawer") |
280 | this._div.innerHTML = this.getHipervinculos() |
281 | } |
282 | |
283 | abre() { |
284 | abreElementoHtml(this._nav) |
285 | abreElementoHtml(this._scrim) |
286 | } |
287 | |
288 | cierra() { |
289 | cierraElementoHtmo(this._nav) |
290 | cierraElementoHtmo(this._scrim) |
291 | } |
292 | |
293 | } |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | @import url(palette.css); |
18 | @import url(typography.css); |
19 | @import url(colors.css); |
20 | @import url(shape.css); |
21 | @import url(motion.css); |
22 | @import url(state.css); |
23 | @import url(elevation.css); |
24 | @import url(theme/light.css) screen and (prefers-color-scheme: light); |
25 | @import url(theme/dark.css) screen and (prefers-color-scheme: dark); |
26 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | .primary { |
18 | color: var(--md-sys-color-on-primary); |
19 | background-color: var(--md-sys-color-primary); |
20 | } |
21 | .on-primary { |
22 | color: var(--md-sys-color-primary); |
23 | background-color: var(--md-sys-color-on-primary); |
24 | } |
25 | .primary-container { |
26 | color: var(--md-sys-color-on-primary-container); |
27 | background-color: var(--md-sys-color-primary-container); |
28 | } |
29 | .on-primary-container { |
30 | color: var(--md-sys-color-primary-container); |
31 | background-color: var(--md-sys-color-on-primary-container); |
32 | } |
33 | .secondary { |
34 | color: var(--md-sys-color-on-secondary); |
35 | background-color: var(--md-sys-color-secondary); |
36 | } |
37 | .on-secondary { |
38 | color: var(--md-sys-color-secondary); |
39 | background-color: var(--md-sys-color-on-secondary); |
40 | } |
41 | .secondary-container { |
42 | color: var(--md-sys-color-on-secondary-container); |
43 | background-color: var(--md-sys-color-secondary-container); |
44 | } |
45 | .on-secondary-container { |
46 | color: var(--md-sys-color-secondary-container); |
47 | background-color: var(--md-sys-color-on-secondary-container); |
48 | } |
49 | .tertiary { |
50 | color: var(--md-sys-color-on-tertiary); |
51 | background-color: var(--md-sys-color-tertiary); |
52 | } |
53 | .on-tertiary { |
54 | color: var(--md-sys-color-tertiary); |
55 | background-color: var(--md-sys-color-on-tertiary); |
56 | } |
57 | .tertiary-container { |
58 | color: var(--md-sys-color-on-tertiary-container); |
59 | background-color: var(--md-sys-color-tertiary-container); |
60 | } |
61 | .on-tertiary-container { |
62 | color: var(--md-sys-color-tertiary-container); |
63 | background-color: var(--md-sys-color-on-tertiary-container); |
64 | } |
65 | .background { |
66 | color: var(--md-sys-color-on-background); |
67 | background-color: var(--md-sys-color-background); |
68 | } |
69 | .surface { |
70 | color: var(--md-sys-color-on-surface); |
71 | background-color: var(--md-sys-color-surface); |
72 | } |
73 | .surface-variant { |
74 | color: var(--md-sys-color-on-surface-variant); |
75 | background-color: var(--md-sys-color-surface-variant); |
76 | } |
77 | .on-surface-variant { |
78 | color: var(--md-sys-color-surface-variant); |
79 | background-color: var(--md-sys-color-on-surface-variant); |
80 | } |
81 | .outline { |
82 | border: 1px solid var(--md-sys-color-outline); |
83 | } |
84 | .inverse-surface { |
85 | color: var(--md-sys-color-on-inverse-surface); |
86 | background-color: var(--md-sys-color-inverse-surface); |
87 | } |
88 | .on-inverse-surface { |
89 | color: var(--md-sys-color-inverse-surface); |
90 | background-color: var(--md-sys-color-on-inverse-surface); |
91 | } |
92 | .inverse-primary { |
93 | color: var(--md-sys-color-on-inverse-primary); |
94 | background-color: var(--md-sys-color-inverse-primary); |
95 | } |
96 | .on-inverse-primary { |
97 | color: var(--md-sys-color-inverse-primary); |
98 | background-color: var(--md-sys-color-on-inverse-primary); |
99 | } |
100 | .surface-tint { |
101 | background-color: var(--md-sys-color-on-surface-tint); |
102 | } |
103 | .error { |
104 | color: var(--md-sys-color-on-error); |
105 | background-color: var(--md-sys-color-error); |
106 | } |
107 | .on-error { |
108 | color: var(--md-sys-color-error); |
109 | background-color: var(--md-sys-color-on-error); |
110 | } |
111 | .error-container { |
112 | color: var(--md-sys-color-on-error-container); |
113 | background-color: var(--md-sys-color-error-container); |
114 | } |
115 | .on-error-container { |
116 | color: var(--md-sys-color-error-container); |
117 | background-color: var(--md-sys-color-on-error-container); |
118 | } |
119 | .black { |
120 | background-color: var(--md-ref-palette-black); |
121 | } |
122 | .black-text { |
123 | color: var(--md-ref-palette-black); |
124 | } |
125 | .white { |
126 | background-color: var(--md-ref-palette-white); |
127 | } |
128 | .white-text { |
129 | color: var(--md-ref-palette-white); |
130 | } |
131 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | :root { |
18 | /* Surface tint color */ |
19 | --md-sys-elevation-surface-tint-color: var(--md-sys-color-primary); |
20 | /* +5 */ |
21 | --md-sys-elevation-level5-value: 12px; |
22 | --md-sys-elevation-level5-unit: 1px; |
23 | --md-sys-elevation-level5: 12px; |
24 | /* +4 */ |
25 | --md-sys-elevation-level4-value: 8px; |
26 | --md-sys-elevation-level4-unit: 1px; |
27 | --md-sys-elevation-level4: 8px; |
28 | /* +3 */ |
29 | --md-sys-elevation-level3-value: 6px; |
30 | --md-sys-elevation-level3-unit: 1px; |
31 | --md-sys-elevation-level3: 6px; |
32 | /* +2 */ |
33 | --md-sys-elevation-level2-value: 3px; |
34 | --md-sys-elevation-level2-unit: 1px; |
35 | --md-sys-elevation-level2: 3px; |
36 | /* +1 */ |
37 | --md-sys-elevation-level1-value: 1px; |
38 | --md-sys-elevation-level1-unit: 1px; |
39 | --md-sys-elevation-level1: 1px; |
40 | /* 0 */ |
41 | --md-sys-elevation-level0-value: 0px; |
42 | --md-sys-elevation-level0-unit: 1px; |
43 | --md-sys-elevation-level0: 0px; |
44 | } |
45 | .elevation-0 { |
46 | box-shadow: var(--md-sys-elevation-level0); |
47 | } |
48 | .elevation-1 { |
49 | box-shadow: var(--md-sys-elevation-level1); |
50 | } |
51 | .elevation-2 { |
52 | box-shadow: var(--md-sys-elevation-level2); |
53 | } |
54 | .elevation-3 { |
55 | box-shadow: var(--md-sys-elevation-level3); |
56 | } |
57 | .elevation-4 { |
58 | box-shadow: var(--md-sys-elevation-level4); |
59 | } |
60 | .elevation-5 { |
61 | box-shadow: var(--md-sys-elevation-level5); |
62 | } |
63 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | :root { |
18 | /* Emphasized decelerate easing (out) */ |
19 | --md-sys-motion-easing-emphasized-decelerate-x0: 0.05000000074505806; |
20 | --md-sys-motion-easing-emphasized-decelerate-y0: 0.699999988079071; |
21 | --md-sys-motion-easing-emphasized-decelerate-x1: 0.10000000149011612; |
22 | --md-sys-motion-easing-emphasized-decelerate-y1: 1; |
23 | /* Emphasized accelerate easing (in) */ |
24 | --md-sys-motion-easing-emphasized-accelerate-x0: 0.30000001192092896; |
25 | --md-sys-motion-easing-emphasized-accelerate-y0: 0; |
26 | --md-sys-motion-easing-emphasized-accelerate-x1: 0.800000011920929; |
27 | --md-sys-motion-easing-emphasized-accelerate-y1: 0.15000000596046448; |
28 | /* Standard decelerate easing (out) */ |
29 | --md-sys-motion-easing-standard-decelerate-x0: 0; |
30 | --md-sys-motion-easing-standard-decelerate-y0: 0; |
31 | --md-sys-motion-easing-standard-decelerate-x1: 0; |
32 | --md-sys-motion-easing-standard-decelerate-y1: 1; |
33 | /* Standard accelerate easing (in) */ |
34 | --md-sys-motion-easing-standard-accelerate-x0: 0.30000001192092896; |
35 | --md-sys-motion-easing-standard-accelerate-y0: 0; |
36 | --md-sys-motion-easing-standard-accelerate-x1: 1; |
37 | --md-sys-motion-easing-standard-accelerate-y1: 1; |
38 | /* Duration 1000ms */ |
39 | --md-sys-motion-duration-1000: 1000ms; |
40 | /* Duration 900ms */ |
41 | --md-sys-motion-duration-900: 900ms; |
42 | /* Duration 800ms */ |
43 | --md-sys-motion-duration-800: 800ms; |
44 | /* Duration 700ms */ |
45 | --md-sys-motion-duration-700: 700ms; |
46 | /* Duration 600ms */ |
47 | --md-sys-motion-duration-600: 600ms; |
48 | /* Duration 550ms */ |
49 | --md-sys-motion-duration-550: 550ms; |
50 | /* Duration 500ms */ |
51 | --md-sys-motion-duration-500: 500ms; |
52 | /* Duration 450ms */ |
53 | --md-sys-motion-duration-450: 450ms; |
54 | /* Duration 400ms */ |
55 | --md-sys-motion-duration-400: 400ms; |
56 | /* Duration 350ms */ |
57 | --md-sys-motion-duration-350: 350ms; |
58 | /* Duration 300ms */ |
59 | --md-sys-motion-duration-300: 300ms; |
60 | /* Duration 250ms */ |
61 | --md-sys-motion-duration-250: 250ms; |
62 | /* Duration 200ms */ |
63 | --md-sys-motion-duration-200: 200ms; |
64 | /* Duration 150ms */ |
65 | --md-sys-motion-duration-150: 150ms; |
66 | /* Duration 100ms */ |
67 | --md-sys-motion-duration-100: 100ms; |
68 | /* Duration 50ms */ |
69 | --md-sys-motion-duration-50: 50ms; |
70 | /* Standard easing (in and out) */ |
71 | --md-sys-motion-easing-standard-x0: 0.20000000298023224; |
72 | --md-sys-motion-easing-standard-y0: 0; |
73 | --md-sys-motion-easing-standard-x1: 0; |
74 | --md-sys-motion-easing-standard-y1: 1; |
75 | /* Linear easing */ |
76 | --md-sys-motion-easing-linear-x0: 0; |
77 | --md-sys-motion-easing-linear-y0: 0; |
78 | --md-sys-motion-easing-linear-x1: 1; |
79 | --md-sys-motion-easing-linear-y1: 1; |
80 | /* Emphasized */ |
81 | --md-sys-motion-easing-emphasized-x0: 0.20000000298023224; |
82 | --md-sys-motion-easing-emphasized-y0: 0; |
83 | --md-sys-motion-easing-emphasized-x1: 0; |
84 | --md-sys-motion-easing-emphasized-y1: 1; |
85 | /* Motion path */ |
86 | --md-sys-motion-path-standard-path: 1; |
87 | } |
88 | .duration-50 { |
89 | transition-duration: var(--md-sys-motion-duration-50); |
90 | } |
91 | .duration-100 { |
92 | transition-duration: var(--md-sys-motion-duration-100); |
93 | } |
94 | .duration-150 { |
95 | transition-duration: var(--md-sys-motion-duration-150); |
96 | } |
97 | .duration-200 { |
98 | transition-duration: var(--md-sys-motion-duration-200); |
99 | } |
100 | .duration-250 { |
101 | transition-duration: var(--md-sys-motion-duration-250); |
102 | } |
103 | .duration-300 { |
104 | transition-duration: var(--md-sys-motion-duration-300); |
105 | } |
106 | .duration-350 { |
107 | transition-duration: var(--md-sys-motion-duration-350); |
108 | } |
109 | .duration-400 { |
110 | transition-duration: var(--md-sys-motion-duration-400); |
111 | } |
112 | .duration-450 { |
113 | transition-duration: var(--md-sys-motion-duration-450); |
114 | } |
115 | .duration-500 { |
116 | transition-duration: var(--md-sys-motion-duration-500); |
117 | } |
118 | .duration-550 { |
119 | transition-duration: var(--md-sys-motion-duration-550); |
120 | } |
121 | .duration-600 { |
122 | transition-duration: var(--md-sys-motion-duration-600); |
123 | } |
124 | .duration-700 { |
125 | transition-duration: var(--md-sys-motion-duration-700); |
126 | } |
127 | .duration-800 { |
128 | transition-duration: var(--md-sys-motion-duration-800); |
129 | } |
130 | .duration-900 { |
131 | transition-duration: var(--md-sys-motion-duration-900); |
132 | } |
133 | .duration-1000 { |
134 | transition-duration: var(--md-sys-motion-duration-1000); |
135 | } |
136 | .easing-standard { |
137 | transition-timing-function: cubic-bezier( |
138 | var(--md-sys-motion-easing-standard-x0), |
139 | var(--md-sys-motion-easing-standard-y0), |
140 | var(--md-sys-motion-easing-standard-x1), |
141 | var(--md-sys-motion-easing-standard-y1) |
142 | ); |
143 | } |
144 | .easing-linear { |
145 | transition-timing-function: cubic-bezier( |
146 | var(--md-sys-motion-easing-linear-x0), |
147 | var(--md-sys-motion-easing-linear-y0), |
148 | var(--md-sys-motion-easing-linear-x1), |
149 | var(--md-sys-motion-easing-linear-y1) |
150 | ); |
151 | } |
152 | .easing-standard-accelerate { |
153 | transition-timing-function: cubic-bezier( |
154 | var(--md-sys-motion-easing-standard-accelerate-x0), |
155 | var(--md-sys-motion-easing-standard-accelerate-y0), |
156 | var(--md-sys-motion-easing-standard-accelerate-x1), |
157 | var(--md-sys-motion-easing-standard-accelerate-y1) |
158 | ); |
159 | } |
160 | .easing-standard-decelerate { |
161 | transition-timing-function: cubic-bezier( |
162 | var(--md-sys-motion-easing-standard-decelerate-x0), |
163 | var(--md-sys-motion-easing-standard-decelerate-y0), |
164 | var(--md-sys-motion-easing-standard-decelerate-x1), |
165 | var(--md-sys-motion-easing-standard-decelerate-y1) |
166 | ); |
167 | } |
168 | .easing-emphasized { |
169 | transition-timing-function: cubic-bezier( |
170 | var(--md-sys-motion-easing-emphasized-x0), |
171 | var(--md-sys-motion-easing-emphasized-y0), |
172 | var(--md-sys-motion-easing-emphasized-x1), |
173 | var(--md-sys-motion-easing-emphasized-y1) |
174 | ); |
175 | } |
176 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | :root { |
18 | /* Error 0 */ |
19 | --md-ref-palette-error0: #000000ff; |
20 | /* Error 10 */ |
21 | --md-ref-palette-error10: #410e0bff; |
22 | /* Error 20 */ |
23 | --md-ref-palette-error20: #601410ff; |
24 | /* Error 30 */ |
25 | --md-ref-palette-error30: #8c1d18ff; |
26 | /* Error 40 */ |
27 | --md-ref-palette-error40: #b3261eff; |
28 | /* Error 50 */ |
29 | --md-ref-palette-error50: #dc362eff; |
30 | /* Error 60 */ |
31 | --md-ref-palette-error60: #e46962ff; |
32 | /* Error 70 */ |
33 | --md-ref-palette-error70: #ec928eff; |
34 | /* Error 80 */ |
35 | --md-ref-palette-error80: #f2b8b5ff; |
36 | /* Error 90 */ |
37 | --md-ref-palette-error90: #f9dedcff; |
38 | /* Error 95 */ |
39 | --md-ref-palette-error95: #fceeeeff; |
40 | /* Error 99 */ |
41 | --md-ref-palette-error99: #fffbf9ff; |
42 | /* Error 100 */ |
43 | --md-ref-palette-error100: #ffffffff; |
44 | /* Tertiary 0 */ |
45 | --md-ref-palette-tertiary0: #000000ff; |
46 | /* Tertiary 10 */ |
47 | --md-ref-palette-tertiary10: #31111dff; |
48 | /* Tertiary 20 */ |
49 | --md-ref-palette-tertiary20: #492532ff; |
50 | /* Tertiary 30 */ |
51 | --md-ref-palette-tertiary30: #633b48ff; |
52 | /* Tertiary 40 */ |
53 | --md-ref-palette-tertiary40: #7d5260ff; |
54 | /* Tertiary 50 */ |
55 | --md-ref-palette-tertiary50: #986977ff; |
56 | /* Tertiary 60 */ |
57 | --md-ref-palette-tertiary60: #b58392ff; |
58 | /* Tertiary 70 */ |
59 | --md-ref-palette-tertiary70: #d29dacff; |
60 | /* Tertiary 80 */ |
61 | --md-ref-palette-tertiary80: #efb8c8ff; |
62 | /* Tertiary 90 */ |
63 | --md-ref-palette-tertiary90: #ffd8e4ff; |
64 | /* Tertiary 95 */ |
65 | --md-ref-palette-tertiary95: #ffecf1ff; |
66 | /* Tertiary 99 */ |
67 | --md-ref-palette-tertiary99: #fffbfaff; |
68 | /* Tertiary 100 */ |
69 | --md-ref-palette-tertiary100: #ffffffff; |
70 | /* Secondary 0 */ |
71 | --md-ref-palette-secondary0: #000000ff; |
72 | /* Secondary 10 */ |
73 | --md-ref-palette-secondary10: #1d192bff; |
74 | /* Secondary 20 */ |
75 | --md-ref-palette-secondary20: #332d41ff; |
76 | /* Secondary 30 */ |
77 | --md-ref-palette-secondary30: #4a4458ff; |
78 | /* Secondary 40 */ |
79 | --md-ref-palette-secondary40: #625b71ff; |
80 | /* Secondary 50 */ |
81 | --md-ref-palette-secondary50: #7a7289ff; |
82 | /* Secondary 60 */ |
83 | --md-ref-palette-secondary60: #958da5ff; |
84 | /* Secondary 70 */ |
85 | --md-ref-palette-secondary70: #b0a7c0ff; |
86 | /* Secondary 80 */ |
87 | --md-ref-palette-secondary80: #ccc2dcff; |
88 | /* Secondary 90 */ |
89 | --md-ref-palette-secondary90: #e8def8ff; |
90 | /* Secondary 95 */ |
91 | --md-ref-palette-secondary95: #f6edffff; |
92 | /* Secondary 99 */ |
93 | --md-ref-palette-secondary99: #fffbfeff; |
94 | /* Secondary 100 */ |
95 | --md-ref-palette-secondary100: #ffffffff; |
96 | /* Primary 0 */ |
97 | --md-ref-palette-primary0: #000000ff; |
98 | /* Primary 10 */ |
99 | --md-ref-palette-primary10: #21005dff; |
100 | /* Primary 20 */ |
101 | --md-ref-palette-primary20: #381e72ff; |
102 | /* Primary 30 */ |
103 | --md-ref-palette-primary30: #4f378bff; |
104 | /* Primary 40 */ |
105 | --md-ref-palette-primary40: #6750a4ff; |
106 | /* Primary 50 */ |
107 | --md-ref-palette-primary50: #7f67beff; |
108 | /* Primary 60 */ |
109 | --md-ref-palette-primary60: #9a82dbff; |
110 | /* Primary 70 */ |
111 | --md-ref-palette-primary70: #b69df8ff; |
112 | /* Primary 80 */ |
113 | --md-ref-palette-primary80: #d0bcffff; |
114 | /* Primary 90 */ |
115 | --md-ref-palette-primary90: #eaddffff; |
116 | /* Primary 95 */ |
117 | --md-ref-palette-primary95: #f6edffff; |
118 | /* Primary 99 */ |
119 | --md-ref-palette-primary99: #fffbfeff; |
120 | /* Primary 100 */ |
121 | --md-ref-palette-primary100: #ffffffff; |
122 | /* Neutral Variant 0 */ |
123 | --md-ref-palette-neutral-variant0: #000000ff; |
124 | /* Neutral Variant 10 */ |
125 | --md-ref-palette-neutral-variant10: #1d1a22ff; |
126 | /* Neutral Variant 20 */ |
127 | --md-ref-palette-neutral-variant20: #322f37ff; |
128 | /* Neutral Variant 30 */ |
129 | --md-ref-palette-neutral-variant30: #49454fff; |
130 | /* Neutral Variant 40 */ |
131 | --md-ref-palette-neutral-variant40: #605d66ff; |
132 | /* Neutral Variant 50 */ |
133 | --md-ref-palette-neutral-variant50: #79747eff; |
134 | /* Neutral Variant 60 */ |
135 | --md-ref-palette-neutral-variant60: #938f99ff; |
136 | /* Neutral Variant 70 */ |
137 | --md-ref-palette-neutral-variant70: #aea9b4ff; |
138 | /* Neutral Variant 80 */ |
139 | --md-ref-palette-neutral-variant80: #cac4d0ff; |
140 | /* Neutral Variant 90 */ |
141 | --md-ref-palette-neutral-variant90: #e7e0ecff; |
142 | /* Neutral Variant 95 */ |
143 | --md-ref-palette-neutral-variant95: #f5eefaff; |
144 | /* Neutral Variant 99 */ |
145 | --md-ref-palette-neutral-variant99: #fffbfeff; |
146 | /* Neutral Variant 100 */ |
147 | --md-ref-palette-neutral-variant100: #ffffffff; |
148 | /* Neutral 0 */ |
149 | --md-ref-palette-neutral0: #000000ff; |
150 | /* Neutral 10 */ |
151 | --md-ref-palette-neutral10: #1c1b1fff; |
152 | /* Neutral 20 */ |
153 | --md-ref-palette-neutral20: #313033ff; |
154 | /* Neutral 30 */ |
155 | --md-ref-palette-neutral30: #484649ff; |
156 | /* Neutral 40 */ |
157 | --md-ref-palette-neutral40: #605d62ff; |
158 | /* Neutral 50 */ |
159 | --md-ref-palette-neutral50: #787579ff; |
160 | /* Neutral 60 */ |
161 | --md-ref-palette-neutral60: #939094ff; |
162 | /* Neutral 70 */ |
163 | --md-ref-palette-neutral70: #aeaaaeff; |
164 | /* Neutral 80 */ |
165 | --md-ref-palette-neutral80: #c9c5caff; |
166 | /* Neutral 90 */ |
167 | --md-ref-palette-neutral90: #e6e1e5ff; |
168 | /* Neutral 95 */ |
169 | --md-ref-palette-neutral95: #f4eff4ff; |
170 | /* Neutral 99 */ |
171 | --md-ref-palette-neutral99: #fffbfeff; |
172 | /* Neutral 100 */ |
173 | --md-ref-palette-neutral100: #ffffffff; |
174 | /* Black */ |
175 | --md-ref-palette-black: #000000ff; |
176 | /* White */ |
177 | --md-ref-palette-white: #ffffffff; |
178 | } |
179 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | :root { |
18 | /* Fully rounded */ |
19 | --md-sys-shape-corner-full-family: 3px; |
20 | /* Extra large top rounding */ |
21 | --md-sys-shape-corner-extra-large-top-family: 1px; |
22 | --md-sys-shape-corner-extra-large-top-default-size: 0px; |
23 | --md-sys-shape-corner-extra-large-top-top-left: 28px; |
24 | --md-sys-shape-corner-extra-large-top-top-right-unit: 1px; |
25 | --md-sys-shape-corner-extra-large-top-top-right: 28px; |
26 | /* Extra large rounding */ |
27 | --md-sys-shape-corner-extra-large-family: 1px; |
28 | --md-sys-shape-corner-extra-large-default-size-unit: 1px; |
29 | --md-sys-shape-corner-extra-large-default-size: 28px; |
30 | /* Large top rounding */ |
31 | --md-sys-shape-corner-large-top-family: 1px; |
32 | --md-sys-shape-corner-large-top-default-size-unit: 1px; |
33 | --md-sys-shape-corner-large-top-default-size: 0px; |
34 | --md-sys-shape-corner-large-top-top-left-unit: 1px; |
35 | --md-sys-shape-corner-large-top-top-left: 16px; |
36 | --md-sys-shape-corner-large-top-top-right-unit: 1px; |
37 | --md-sys-shape-corner-large-top-top-right: 16px; |
38 | /* Large end rounding */ |
39 | --md-sys-shape-corner-large-end-family: 1px; |
40 | --md-sys-shape-corner-large-end-default-size-unit: 1px; |
41 | --md-sys-shape-corner-large-end-default-size: 0px; |
42 | --md-sys-shape-corner-large-end-top-right-unit: 1px; |
43 | --md-sys-shape-corner-large-end-top-right: 16px; |
44 | --md-sys-shape-corner-large-end-bottom-right-unit: 1px; |
45 | --md-sys-shape-corner-large-end-bottom-right: 16px; |
46 | /* Large rounding */ |
47 | --md-sys-shape-corner-large-family: 1px; |
48 | --md-sys-shape-corner-large-default-size-unit: 1px; |
49 | --md-sys-shape-corner-large-default-size: 16px; |
50 | /* Medium rounding */ |
51 | --md-sys-shape-corner-medium-family: 1px; |
52 | --md-sys-shape-corner-medium-default-size-unit: 1px; |
53 | --md-sys-shape-corner-medium-default-size: 12px; |
54 | /* Small rounding */ |
55 | --md-sys-shape-corner-small-family: 1px; |
56 | --md-sys-shape-corner-small-default-size-unit: 1px; |
57 | --md-sys-shape-corner-small-default-size: 8px; |
58 | /* Extra small top rounding */ |
59 | --md-sys-shape-corner-extra-small-top-family: 1px; |
60 | --md-sys-shape-corner-extra-small-top-default-size-unit: 1px; |
61 | --md-sys-shape-corner-extra-small-top-default-size: 0px; |
62 | --md-sys-shape-corner-extra-small-top-top-left-unit: 1px; |
63 | --md-sys-shape-corner-extra-small-top-top-left: 4px; |
64 | --md-sys-shape-corner-extra-small-top-top-right-unit: 1px; |
65 | --md-sys-shape-corner-extra-small-top-top-right: 4px; |
66 | /* Extra small rounding */ |
67 | --md-sys-shape-corner-extra-small-family: 1px; |
68 | --md-sys-shape-corner-extra-small-default-size-unit: 1px; |
69 | --md-sys-shape-corner-extra-small-default-size: 4px; |
70 | /* No rounding */ |
71 | --md-sys-shape-corner-none-family: 1px; |
72 | --md-sys-shape-corner-none-default-size-unit: 1px; |
73 | --md-sys-shape-corner-none-default-size: 0px; |
74 | |
75 | --md-sys-shape-small: var(--md-sys-shape-corner-small-default-size); |
76 | --md-sys-shape-medium: var(--md-sys-shape-corner-medium-default-size); |
77 | --md-sys-shape-large: var(--md-sys-shape-corner-large-default-size); |
78 | } |
79 | |
80 | .shape-none { |
81 | border-radius: var(--md-sys-shape-corner-none-default-size); |
82 | } |
83 | .shape-extra-small { |
84 | border-radius: var(--md-sys-shape-corner-extra-small-default-size); |
85 | } |
86 | .shape-small { |
87 | border-radius: var(--md-sys-shape-corner-small-default-size); |
88 | } |
89 | .shape-medium { |
90 | border-radius: var(--md-sys-shape-corner-medium-default-size); |
91 | } |
92 | .shape-large { |
93 | border-radius: var(--md-sys-shape-corner-large-default-size); |
94 | } |
95 | .shape-extra-large { |
96 | border-radius: var(--md-sys-shape-corner-extra-large-default-size); |
97 | } |
98 | .extra-small-top { |
99 | border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left); |
100 | border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right); |
101 | } |
102 | .large-end { |
103 | border-top-right-radius: var(--md-sys-shape-corner-large-end-top-right); |
104 | border-bottom-right-radius: var(--md-sys-shape-corner-large-end-bottom-right); |
105 | } |
106 | .large-top { |
107 | border-top-left-radius: var(--md-sys-shape-corner-large-top-top-left); |
108 | border-top-right-radius: var(--md-sys-shape-corner-large-top-top-right); |
109 | } |
110 | .extra-large-top { |
111 | border-top-left-radius: var(--md-sys-shape-corner-extra-large-top-top-left); |
112 | border-top-right-radius: var(--md-sys-shape-corner-extra-large-top-top-right); |
113 | } |
114 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | :root { |
18 | /* Dragged state layer opacity */ |
19 | --md-sys-state-dragged-state-layer-opacity: 0.1599999964237213; |
20 | /* Pressed state layer opacity */ |
21 | --md-sys-state-pressed-state-layer-opacity: 0.11999999731779099; |
22 | /* Focus state layer opacity */ |
23 | --md-sys-state-focus-state-layer-opacity: 0.11999999731779099; |
24 | /* Hover state layer opacity */ |
25 | --md-sys-state-hover-state-layer-opacity: 0.07999999821186066; |
26 | } |
27 | .hover-state-layer { |
28 | opacity: var(--md-sys-state-hover-state-layer-opacity); |
29 | } |
30 | .pressed-state-layer { |
31 | opacity: var(--md-sys-state-pressed-state-layer-opacity); |
32 | } |
33 | .dragged-state-layer { |
34 | opacity: var(--md-sys-state-dragged-state-layer-opacity); |
35 | } |
36 | .focus-state-layer { |
37 | opacity: var(--md-sys-state-focus-state-layer-opacity); |
38 | } |
39 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | /* This file is generated */ |
18 | |
19 | /* DO NOT EDIT */ |
20 | |
21 | :root { |
22 | /* Label Small */ |
23 | --md-sys-typescale-label-small-text-transform: unset; |
24 | --md-sys-typescale-label-small-axis-value: unset; |
25 | --md-sys-typescale-label-small-font-style: unset; |
26 | --md-sys-typescale-label-small-text-decoration: unset; |
27 | /* Label Small line height */ |
28 | --md-sys-typescale-label-small-line-height-value: 16px; |
29 | --md-sys-typescale-label-small-line-height-unit: 2px; |
30 | --md-sys-typescale-label-small-line-height: 16px; |
31 | /* Label Small font tracking */ |
32 | --md-sys-typescale-label-small-tracking-value: 0.5px; |
33 | --md-sys-typescale-label-small-tracking-unit: 2px; |
34 | --md-sys-typescale-label-small-tracking: 0.5px; |
35 | /* Label Small font size */ |
36 | --md-sys-typescale-label-small-size-value: 11px; |
37 | --md-sys-typescale-label-small-size-unit: 2px; |
38 | --md-sys-typescale-label-small-size: 11px; |
39 | /* Label Small font weight */ |
40 | --md-sys-typescale-label-small-weight: var(--md-ref-typeface-weight-medium); |
41 | /* Label Small font name */ |
42 | --md-sys-typescale-label-small-font: var(--md-ref-typeface-plain); |
43 | /* Label Medium */ |
44 | --md-sys-typescale-label-medium-axis-value: unset; |
45 | --md-sys-typescale-label-medium-font-style: unset; |
46 | --md-sys-typescale-label-medium-text-decoration: unset; |
47 | /* Label Medium text transform */ |
48 | --md-sys-typescale-label-medium-text-transform: 1; |
49 | /* Label Medium line height */ |
50 | --md-sys-typescale-label-medium-line-height-value: 16px; |
51 | --md-sys-typescale-label-medium-line-height-unit: 2px; |
52 | --md-sys-typescale-label-medium-line-height: 16px; |
53 | /* Label Medium font tracking */ |
54 | --md-sys-typescale-label-medium-tracking-value: 0.5px; |
55 | --md-sys-typescale-label-medium-tracking-unit: 2px; |
56 | --md-sys-typescale-label-medium-tracking: 0.5px; |
57 | /* Label Medium font size */ |
58 | --md-sys-typescale-label-medium-size-value: 12px; |
59 | --md-sys-typescale-label-medium-size-unit: 2px; |
60 | --md-sys-typescale-label-medium-size: 12px; |
61 | /* Label Medium font weight */ |
62 | --md-sys-typescale-label-medium-weight: var(--md-ref-typeface-weight-medium); |
63 | /* Label Medium font name */ |
64 | --md-sys-typescale-label-medium-font: var(--md-ref-typeface-plain); |
65 | /* Label Large */ |
66 | --md-sys-typescale-label-large-text-transform: unset; |
67 | --md-sys-typescale-label-large-axis-value: unset; |
68 | --md-sys-typescale-label-large-font-style: unset; |
69 | --md-sys-typescale-label-large-text-decoration: unset; |
70 | /* Label Large line height */ |
71 | --md-sys-typescale-label-large-line-height-value: 20px; |
72 | --md-sys-typescale-label-large-line-height-unit: 2px; |
73 | --md-sys-typescale-label-large-line-height: 20px; |
74 | /* Label Large font tracking */ |
75 | --md-sys-typescale-label-large-tracking-value: 0.10000000149011612px; |
76 | --md-sys-typescale-label-large-tracking-unit: 2px; |
77 | --md-sys-typescale-label-large-tracking: 0.10000000149011612px; |
78 | /* Label Large font size */ |
79 | --md-sys-typescale-label-large-size-value: 14px; |
80 | --md-sys-typescale-label-large-size-unit: 2px; |
81 | --md-sys-typescale-label-large-size: 14px; |
82 | /* Label Large font weight */ |
83 | --md-sys-typescale-label-large-weight: var(--md-ref-typeface-weight-medium); |
84 | /* Label Large font name */ |
85 | --md-sys-typescale-label-large-font: var(--md-ref-typeface-plain); |
86 | /* Body Small */ |
87 | --md-sys-typescale-body-small-text-transform: unset; |
88 | --md-sys-typescale-body-small-axis-value: unset; |
89 | --md-sys-typescale-body-small-font-style: unset; |
90 | --md-sys-typescale-body-small-text-decoration: unset; |
91 | /* Body Small line height */ |
92 | --md-sys-typescale-body-small-line-height-value: 16px; |
93 | --md-sys-typescale-body-small-line-height-unit: 2px; |
94 | --md-sys-typescale-body-small-line-height: 16px; |
95 | /* Body Small font tracking */ |
96 | --md-sys-typescale-body-small-tracking-value: 0.4000000059604645px; |
97 | --md-sys-typescale-body-small-tracking-unit: 2px; |
98 | --md-sys-typescale-body-small-tracking: 0.4000000059604645px; |
99 | /* Body Small font size */ |
100 | --md-sys-typescale-body-small-size-value: 12px; |
101 | --md-sys-typescale-body-small-size-unit: 2px; |
102 | --md-sys-typescale-body-small-size: 12px; |
103 | /* Body Small font weight */ |
104 | --md-sys-typescale-body-small-weight: var(--md-ref-typeface-weight-regular); |
105 | /* Body Small font name */ |
106 | --md-sys-typescale-body-small-font: var(--md-ref-typeface-plain); |
107 | /* Body Medium */ |
108 | --md-sys-typescale-body-medium-text-transform: unset; |
109 | --md-sys-typescale-body-medium-axis-value: unset; |
110 | --md-sys-typescale-body-medium-font-style: unset; |
111 | --md-sys-typescale-body-medium-text-decoration: unset; |
112 | /* Body Medium line height */ |
113 | --md-sys-typescale-body-medium-line-height-value: 20px; |
114 | --md-sys-typescale-body-medium-line-height-unit: 2px; |
115 | --md-sys-typescale-body-medium-line-height: 20px; |
116 | /* Body Medium font tracking */ |
117 | --md-sys-typescale-body-medium-tracking-value: 0.25px; |
118 | --md-sys-typescale-body-medium-tracking-unit: 2px; |
119 | --md-sys-typescale-body-medium-tracking: 0.25px; |
120 | /* Body Medium font size */ |
121 | --md-sys-typescale-body-medium-size-value: 14px; |
122 | --md-sys-typescale-body-medium-size-unit: 2px; |
123 | --md-sys-typescale-body-medium-size: 14px; |
124 | /* Body Medium font weight */ |
125 | --md-sys-typescale-body-medium-weight: var(--md-ref-typeface-weight-regular); |
126 | /* Body Medium font name */ |
127 | --md-sys-typescale-body-medium-font: var(--md-ref-typeface-plain); |
128 | /* Body Large */ |
129 | --md-sys-typescale-body-large-text-transform: unset; |
130 | --md-sys-typescale-body-large-axis-value: unset; |
131 | --md-sys-typescale-body-large-font-style: unset; |
132 | --md-sys-typescale-body-large-text-decoration: unset; |
133 | /* Body Large line height */ |
134 | --md-sys-typescale-body-large-line-height-value: 24px; |
135 | --md-sys-typescale-body-large-line-height-unit: 2px; |
136 | --md-sys-typescale-body-large-line-height: 24px; |
137 | /* Body Large font tracking */ |
138 | --md-sys-typescale-body-large-tracking-value: 0.5px; |
139 | --md-sys-typescale-body-large-tracking-unit: 2px; |
140 | --md-sys-typescale-body-large-tracking: 0.5px; |
141 | /* Body Large font size */ |
142 | --md-sys-typescale-body-large-size-value: 16px; |
143 | --md-sys-typescale-body-large-size-unit: 2px; |
144 | --md-sys-typescale-body-large-size: 16px; |
145 | /* Body Large font weight */ |
146 | --md-sys-typescale-body-large-weight: var(--md-ref-typeface-weight-regular); |
147 | /* Body Large font name */ |
148 | --md-sys-typescale-body-large-font: var(--md-ref-typeface-plain); |
149 | /* Title Small */ |
150 | --md-sys-typescale-title-small-text-transform: unset; |
151 | --md-sys-typescale-title-small-axis-value: unset; |
152 | --md-sys-typescale-title-small-font-style: unset; |
153 | --md-sys-typescale-title-small-text-decoration: unset; |
154 | /* Title Small line height */ |
155 | --md-sys-typescale-title-small-line-height-value: 20px; |
156 | --md-sys-typescale-title-small-line-height-unit: 2px; |
157 | --md-sys-typescale-title-small-line-height: 20px; |
158 | /* Title Small font tracking */ |
159 | --md-sys-typescale-title-small-tracking-value: 0.10000000149011612px; |
160 | --md-sys-typescale-title-small-tracking-unit: 2px; |
161 | --md-sys-typescale-title-small-tracking: 0.10000000149011612px; |
162 | /* Title Small font size */ |
163 | --md-sys-typescale-title-small-size-value: 14px; |
164 | --md-sys-typescale-title-small-size-unit: 2px; |
165 | --md-sys-typescale-title-small-size: 14px; |
166 | /* Title Small font weight */ |
167 | --md-sys-typescale-title-small-weight: var(--md-ref-typeface-weight-medium); |
168 | /* Title Small font name */ |
169 | --md-sys-typescale-title-small-font: var(--md-ref-typeface-plain); |
170 | /* Title Medium */ |
171 | --md-sys-typescale-title-medium-text-transform: unset; |
172 | --md-sys-typescale-title-medium-axis-value: unset; |
173 | --md-sys-typescale-title-medium-font-style: unset; |
174 | --md-sys-typescale-title-medium-text-decoration: unset; |
175 | /* Title Medium line height */ |
176 | --md-sys-typescale-title-medium-line-height-value: 24px; |
177 | --md-sys-typescale-title-medium-line-height-unit: 2px; |
178 | --md-sys-typescale-title-medium-line-height: 24px; |
179 | /* Title Medium font tracking */ |
180 | --md-sys-typescale-title-medium-tracking-value: 0.15000000596046448px; |
181 | --md-sys-typescale-title-medium-tracking-unit: 2px; |
182 | --md-sys-typescale-title-medium-tracking: 0.15000000596046448px; |
183 | /* Title Medium font size */ |
184 | --md-sys-typescale-title-medium-size-value: 16px; |
185 | --md-sys-typescale-title-medium-size-unit: 2px; |
186 | --md-sys-typescale-title-medium-size: 16px; |
187 | /* Title Medium font weight */ |
188 | --md-sys-typescale-title-medium-weight: var(--md-ref-typeface-weight-medium); |
189 | /* Title Medium font name */ |
190 | --md-sys-typescale-title-medium-font: var(--md-ref-typeface-plain); |
191 | /* Title Large */ |
192 | --md-sys-typescale-title-large-text-transform: unset; |
193 | --md-sys-typescale-title-large-axis-value: unset; |
194 | --md-sys-typescale-title-large-font-style: unset; |
195 | --md-sys-typescale-title-large-text-decoration: unset; |
196 | /* Title Large line height */ |
197 | --md-sys-typescale-title-large-line-height-value: 28px; |
198 | --md-sys-typescale-title-large-line-height-unit: 2px; |
199 | --md-sys-typescale-title-large-line-height: 28px; |
200 | /* Title Large font tracking */ |
201 | --md-sys-typescale-title-large-tracking-value: 0px; |
202 | --md-sys-typescale-title-large-tracking-unit: 2px; |
203 | --md-sys-typescale-title-large-tracking: 0px; |
204 | /* Title Large font size */ |
205 | --md-sys-typescale-title-large-size-value: 22px; |
206 | --md-sys-typescale-title-large-size-unit: 2px; |
207 | --md-sys-typescale-title-large-size: 22px; |
208 | /* Title Large font weight */ |
209 | --md-sys-typescale-title-large-weight: var(--md-ref-typeface-weight-regular); |
210 | /* Title Large font name */ |
211 | --md-sys-typescale-title-large-font: var(--md-ref-typeface-brand); |
212 | /* Headline Small */ |
213 | --md-sys-typescale-headline-small-text-transform: unset; |
214 | --md-sys-typescale-headline-small-axis-value: unset; |
215 | --md-sys-typescale-headline-small-font-style: unset; |
216 | --md-sys-typescale-headline-small-text-decoration: unset; |
217 | /* Headline Small line height */ |
218 | --md-sys-typescale-headline-small-line-height-value: 32px; |
219 | --md-sys-typescale-headline-small-line-height-unit: 2px; |
220 | --md-sys-typescale-headline-small-line-height: 32px; |
221 | /* Headline Small font tracking */ |
222 | --md-sys-typescale-headline-small-tracking-value: 0px; |
223 | --md-sys-typescale-headline-small-tracking-unit: 2px; |
224 | --md-sys-typescale-headline-small-tracking: 0px; |
225 | /* Headline Small font size */ |
226 | --md-sys-typescale-headline-small-size-value: 24px; |
227 | --md-sys-typescale-headline-small-size-unit: 2px; |
228 | --md-sys-typescale-headline-small-size: 24px; |
229 | /* Headline Small font weight */ |
230 | --md-sys-typescale-headline-small-weight: var( |
231 | --md-ref-typeface-weight-regular |
232 | ); |
233 | /* Headline Small font name */ |
234 | --md-sys-typescale-headline-small-font: var(--md-ref-typeface-brand); |
235 | /* Headline Medium */ |
236 | --md-sys-typescale-headline-medium-text-transform: unset; |
237 | --md-sys-typescale-headline-medium-axis-value: unset; |
238 | --md-sys-typescale-headline-medium-font-style: unset; |
239 | --md-sys-typescale-headline-medium-text-decoration: unset; |
240 | /* Headline Medium line height */ |
241 | --md-sys-typescale-headline-medium-line-height-value: 36px; |
242 | --md-sys-typescale-headline-medium-line-height-unit: 2px; |
243 | --md-sys-typescale-headline-medium-line-height: 36px; |
244 | /* Headline Medium font tracking */ |
245 | --md-sys-typescale-headline-medium-tracking-value: 0px; |
246 | --md-sys-typescale-headline-medium-tracking-unit: 2px; |
247 | --md-sys-typescale-headline-medium-tracking: 0px; |
248 | /* Headline Medium font size */ |
249 | --md-sys-typescale-headline-medium-size-value: 28px; |
250 | --md-sys-typescale-headline-medium-size-unit: 2px; |
251 | --md-sys-typescale-headline-medium-size: 28px; |
252 | /* Headline Medium font weight */ |
253 | --md-sys-typescale-headline-medium-weight: var( |
254 | --md-ref-typeface-weight-regular |
255 | ); |
256 | /* Headline Medium font name */ |
257 | --md-sys-typescale-headline-medium-font: var(--md-ref-typeface-brand); |
258 | /* Headline Large */ |
259 | --md-sys-typescale-headline-large-text-transform: unset; |
260 | --md-sys-typescale-headline-large-axis-value: unset; |
261 | --md-sys-typescale-headline-large-font-style: unset; |
262 | --md-sys-typescale-headline-large-text-decoration: unset; |
263 | /* Headline Large line height */ |
264 | --md-sys-typescale-headline-large-line-height-value: 40px; |
265 | --md-sys-typescale-headline-large-line-height-unit: 2px; |
266 | --md-sys-typescale-headline-large-line-height: 40px; |
267 | /* Headline Large font tracking */ |
268 | --md-sys-typescale-headline-large-tracking-value: 0px; |
269 | --md-sys-typescale-headline-large-tracking-unit: 2px; |
270 | --md-sys-typescale-headline-large-tracking: 0px; |
271 | /* Headline Large font size */ |
272 | --md-sys-typescale-headline-large-size-value: 32px; |
273 | --md-sys-typescale-headline-large-size-unit: 2px; |
274 | --md-sys-typescale-headline-large-size: 32px; |
275 | /* Headline Large font name */ |
276 | --md-sys-typescale-headline-large-font: var(--md-ref-typeface-brand); |
277 | /* Headline Large font weight */ |
278 | --md-sys-typescale-headline-large-weight: var( |
279 | --md-ref-typeface-weight-regular |
280 | ); |
281 | /* Display Small */ |
282 | --md-sys-typescale-display-small-text-transform: unset; |
283 | --md-sys-typescale-display-small-axis-value: unset; |
284 | --md-sys-typescale-display-small-font-style: unset; |
285 | --md-sys-typescale-display-small-text-decoration: unset; |
286 | /* Display Small line height */ |
287 | --md-sys-typescale-display-small-line-height-value: 44px; |
288 | --md-sys-typescale-display-small-line-height-unit: 2px; |
289 | --md-sys-typescale-display-small-line-height: 44px; |
290 | /* Display Small font tracking */ |
291 | --md-sys-typescale-display-small-tracking-value: 0px; |
292 | --md-sys-typescale-display-small-tracking-unit: 2px; |
293 | --md-sys-typescale-display-small-tracking: 0px; |
294 | /* Display Small font size */ |
295 | --md-sys-typescale-display-small-size-value: 36px; |
296 | --md-sys-typescale-display-small-size-unit: 2px; |
297 | --md-sys-typescale-display-small-size: 36px; |
298 | /* Display Small font weight */ |
299 | --md-sys-typescale-display-small-weight: var( |
300 | --md-ref-typeface-weight-regular |
301 | ); |
302 | /* Display Small font name */ |
303 | --md-sys-typescale-display-small-font: var(--md-ref-typeface-brand); |
304 | /* Display Medium */ |
305 | --md-sys-typescale-display-medium-text-transform: unset; |
306 | --md-sys-typescale-display-medium-axis-value: unset; |
307 | --md-sys-typescale-display-medium-font-style: unset; |
308 | --md-sys-typescale-display-medium-text-decoration: unset; |
309 | /* Display Medium line height */ |
310 | --md-sys-typescale-display-medium-line-height-value: 52px; |
311 | --md-sys-typescale-display-medium-line-height-unit: 2px; |
312 | --md-sys-typescale-display-medium-line-height: 52px; |
313 | /* Display Medium font tracking */ |
314 | --md-sys-typescale-display-medium-tracking-value: 0px; |
315 | --md-sys-typescale-display-medium-tracking-unit: 2px; |
316 | --md-sys-typescale-display-medium-tracking: 0px; |
317 | /* Display Medium font size */ |
318 | --md-sys-typescale-display-medium-size-value: 45px; |
319 | --md-sys-typescale-display-medium-size-unit: 2px; |
320 | --md-sys-typescale-display-medium-size: 45px; |
321 | /* Display Medium font weight */ |
322 | --md-sys-typescale-display-medium-weight: var( |
323 | --md-ref-typeface-weight-regular |
324 | ); |
325 | /* Display Medium font name */ |
326 | --md-sys-typescale-display-medium-font: var(--md-ref-typeface-brand); |
327 | /* Display Large */ |
328 | --md-sys-typescale-display-large-text-transform: unset; |
329 | --md-sys-typescale-display-large-axis-value: unset; |
330 | --md-sys-typescale-display-large-font-style: unset; |
331 | --md-sys-typescale-display-large-text-decoration: unset; |
332 | /* Display Large line height */ |
333 | --md-sys-typescale-display-large-line-height-value: 64px; |
334 | --md-sys-typescale-display-large-line-height-unit: 2px; |
335 | --md-sys-typescale-display-large-line-height: 64px; |
336 | /* Display Large font tracking */ |
337 | --md-sys-typescale-display-large-tracking-value: -0.25px; |
338 | --md-sys-typescale-display-large-tracking-unit: 2px; |
339 | --md-sys-typescale-display-large-tracking: -0.25px; |
340 | /* Display Large font size */ |
341 | --md-sys-typescale-display-large-size-value: 57px; |
342 | --md-sys-typescale-display-large-size-unit: 2px; |
343 | --md-sys-typescale-display-large-size: 57px; |
344 | /* Display Large font weight */ |
345 | --md-sys-typescale-display-large-weight: var( |
346 | --md-ref-typeface-weight-regular |
347 | ); |
348 | /* Display Large font name */ |
349 | --md-sys-typescale-display-large-font: var(--md-ref-typeface-brand); |
350 | /* Plain typeface */ |
351 | --md-ref-typeface-plain: Roboto; |
352 | /* Brand typeface */ |
353 | --md-ref-typeface-brand: Roboto; |
354 | /* Bold weight */ |
355 | --md-ref-typeface-weight-bold: 700; |
356 | /* Medium weight */ |
357 | --md-ref-typeface-weight-medium: 500; |
358 | /* Regular weight */ |
359 | --md-ref-typeface-weight-regular: 400; |
360 | } |
361 | |
362 | /* Label Small */ |
363 | .label-small { |
364 | font-family: var(--md-sys-typescale-label-small-font); |
365 | font-weight: var(--md-sys-typescale-label-small-weight); |
366 | font-size: var(--md-sys-typescale-label-small-size); |
367 | font-style: var(--md-sys-typescale-label-small-font-style); |
368 | letter-spacing: var(--md-sys-typescale-label-small-tracking); |
369 | line-height: var(--md-sys-typescale-label-small-line-height); |
370 | text-transform: var(--md-sys-typescale-label-small-text-transform); |
371 | text-decoration: var(--md-sys-typescale-label-small-text-decoration); |
372 | } |
373 | /* Label Medium */ |
374 | .label-medium { |
375 | font-family: var(--md-sys-typescale-label-medium-font); |
376 | font-weight: var(--md-sys-typescale-label-medium-weight); |
377 | font-size: var(--md-sys-typescale-label-medium-size); |
378 | font-style: var(--md-sys-typescale-label-medium-font-style); |
379 | letter-spacing: var(--md-sys-typescale-label-medium-tracking); |
380 | line-height: var(--md-sys-typescale-label-medium-line-height); |
381 | text-transform: var(--md-sys-typescale-label-medium-text-transform); |
382 | text-decoration: var(--md-sys-typescale-label-medium-text-decoration); |
383 | } |
384 | /* Label Large */ |
385 | .label-large { |
386 | font-family: var(--md-sys-typescale-label-large-font); |
387 | font-weight: var(--md-sys-typescale-label-large-weight); |
388 | font-size: var(--md-sys-typescale-label-large-size); |
389 | font-style: var(--md-sys-typescale-label-large-font-style); |
390 | letter-spacing: var(--md-sys-typescale-label-large-tracking); |
391 | line-height: var(--md-sys-typescale-label-large-line-height); |
392 | text-transform: var(--md-sys-typescale-label-large-text-transform); |
393 | text-decoration: var(--md-sys-typescale-label-large-text-decoration); |
394 | } |
395 | /* Body Small */ |
396 | .body-small { |
397 | font-family: var(--md-sys-typescale-body-small-font); |
398 | font-weight: var(--md-sys-typescale-body-small-weight); |
399 | font-size: var(--md-sys-typescale-body-small-size); |
400 | font-style: var(--md-sys-typescale-body-small-font-style); |
401 | letter-spacing: var(--md-sys-typescale-body-small-tracking); |
402 | line-height: var(--md-sys-typescale-body-small-line-height); |
403 | text-transform: var(--md-sys-typescale-body-small-text-transform); |
404 | text-decoration: var(--md-sys-typescale-body-small-text-decoration); |
405 | } |
406 | /* Body Medium */ |
407 | .body-medium { |
408 | font-family: var(--md-sys-typescale-body-medium-font); |
409 | font-weight: var(--md-sys-typescale-body-medium-weight); |
410 | font-size: var(--md-sys-typescale-body-medium-size); |
411 | font-style: var(--md-sys-typescale-body-medium-font-style); |
412 | letter-spacing: var(--md-sys-typescale-body-medium-tracking); |
413 | line-height: var(--md-sys-typescale-body-medium-line-height); |
414 | text-transform: var(--md-sys-typescale-body-medium-text-transform); |
415 | text-decoration: var(--md-sys-typescale-body-medium-text-decoration); |
416 | } |
417 | /* Body Large */ |
418 | .body-large { |
419 | font-family: var(--md-sys-typescale-body-large-font); |
420 | font-weight: var(--md-sys-typescale-body-large-weight); |
421 | font-size: var(--md-sys-typescale-body-large-size); |
422 | font-style: var(--md-sys-typescale-body-large-font-style); |
423 | letter-spacing: var(--md-sys-typescale-body-large-tracking); |
424 | line-height: var(--md-sys-typescale-body-large-line-height); |
425 | text-transform: var(--md-sys-typescale-body-large-text-transform); |
426 | text-decoration: var(--md-sys-typescale-body-large-text-decoration); |
427 | } |
428 | /* Title Small */ |
429 | .title-small { |
430 | font-family: var(--md-sys-typescale-title-small-font); |
431 | font-weight: var(--md-sys-typescale-title-small-weight); |
432 | font-size: var(--md-sys-typescale-title-small-size); |
433 | font-style: var(--md-sys-typescale-title-small-font-style); |
434 | letter-spacing: var(--md-sys-typescale-title-small-tracking); |
435 | line-height: var(--md-sys-typescale-title-small-line-height); |
436 | text-transform: var(--md-sys-typescale-title-small-text-transform); |
437 | text-decoration: var(--md-sys-typescale-title-small-text-decoration); |
438 | } |
439 | /* Title Medium */ |
440 | .title-medium { |
441 | font-family: var(--md-sys-typescale-title-medium-font); |
442 | font-weight: var(--md-sys-typescale-title-medium-weight); |
443 | font-size: var(--md-sys-typescale-title-medium-size); |
444 | font-style: var(--md-sys-typescale-title-medium-font-style); |
445 | letter-spacing: var(--md-sys-typescale-title-medium-tracking); |
446 | line-height: var(--md-sys-typescale-title-medium-line-height); |
447 | text-transform: var(--md-sys-typescale-title-medium-text-transform); |
448 | text-decoration: var(--md-sys-typescale-title-medium-text-decoration); |
449 | } |
450 | /* Title Large */ |
451 | .title-large { |
452 | font-family: var(--md-sys-typescale-title-large-font); |
453 | font-weight: var(--md-sys-typescale-title-large-weight); |
454 | font-size: var(--md-sys-typescale-title-large-size); |
455 | font-style: var(--md-sys-typescale-title-large-font-style); |
456 | letter-spacing: var(--md-sys-typescale-title-large-tracking); |
457 | line-height: var(--md-sys-typescale-title-large-line-height); |
458 | text-transform: var(--md-sys-typescale-title-large-text-transform); |
459 | text-decoration: var(--md-sys-typescale-title-large-text-decoration); |
460 | } |
461 | /* Headline Small */ |
462 | .headline-small { |
463 | font-family: var(--md-sys-typescale-headline-small-font); |
464 | font-weight: var(--md-sys-typescale-headline-small-weight); |
465 | font-size: var(--md-sys-typescale-headline-small-size); |
466 | font-style: var(--md-sys-typescale-headline-small-font-style); |
467 | letter-spacing: var(--md-sys-typescale-headline-small-tracking); |
468 | line-height: var(--md-sys-typescale-headline-small-line-height); |
469 | text-transform: var(--md-sys-typescale-headline-small-text-transform); |
470 | text-decoration: var(--md-sys-typescale-headline-small-text-decoration); |
471 | } |
472 | /* Headline Medium */ |
473 | .headline-medium { |
474 | font-family: var(--md-sys-typescale-headline-medium-font); |
475 | font-weight: var(--md-sys-typescale-headline-medium-weight); |
476 | font-size: var(--md-sys-typescale-headline-medium-size); |
477 | font-style: var(--md-sys-typescale-headline-medium-font-style); |
478 | letter-spacing: var(--md-sys-typescale-headline-medium-tracking); |
479 | line-height: var(--md-sys-typescale-headline-medium-line-height); |
480 | text-transform: var(--md-sys-typescale-headline-medium-text-transform); |
481 | text-decoration: var(--md-sys-typescale-headline-medium-text-decoration); |
482 | } |
483 | /* Headline Large */ |
484 | .headline-large { |
485 | font-family: var(--md-sys-typescale-headline-large-font); |
486 | font-weight: var(--md-sys-typescale-headline-large-weight); |
487 | font-size: var(--md-sys-typescale-headline-large-size); |
488 | font-style: var(--md-sys-typescale-headline-large-font-style); |
489 | letter-spacing: var(--md-sys-typescale-headline-large-tracking); |
490 | line-height: var(--md-sys-typescale-headline-large-line-height); |
491 | text-transform: var(--md-sys-typescale-headline-large-text-transform); |
492 | text-decoration: var(--md-sys-typescale-headline-large-text-decoration); |
493 | } |
494 | /* Display Small */ |
495 | .display-small { |
496 | font-family: var(--md-sys-typescale-display-small-font); |
497 | font-weight: var(--md-sys-typescale-display-small-weight); |
498 | font-size: var(--md-sys-typescale-display-small-size); |
499 | font-style: var(--md-sys-typescale-display-small-font-style); |
500 | letter-spacing: var(--md-sys-typescale-display-small-tracking); |
501 | line-height: var(--md-sys-typescale-display-small-line-height); |
502 | text-transform: var(--md-sys-typescale-display-small-text-transform); |
503 | text-decoration: var(--md-sys-typescale-display-small-text-decoration); |
504 | } |
505 | /* Display Medium */ |
506 | .display-medium { |
507 | font-family: var(--md-sys-typescale-display-medium-font); |
508 | font-weight: var(--md-sys-typescale-display-medium-weight); |
509 | font-size: var(--md-sys-typescale-display-medium-size); |
510 | font-style: var(--md-sys-typescale-display-medium-font-style); |
511 | letter-spacing: var(--md-sys-typescale-display-medium-tracking); |
512 | line-height: var(--md-sys-typescale-display-medium-line-height); |
513 | text-transform: var(--md-sys-typescale-display-medium-text-transform); |
514 | text-decoration: var(--md-sys-typescale-display-medium-text-decoration); |
515 | } |
516 | /* Display Large */ |
517 | .display-large { |
518 | font-family: var(--md-sys-typescale-display-large-font); |
519 | font-weight: var(--md-sys-typescale-display-large-weight); |
520 | font-size: var(--md-sys-typescale-display-large-size); |
521 | font-style: var(--md-sys-typescale-display-large-font-style); |
522 | letter-spacing: var(--md-sys-typescale-display-large-tracking); |
523 | line-height: var(--md-sys-typescale-display-large-line-height); |
524 | text-transform: var(--md-sys-typescale-display-large-text-transform); |
525 | text-decoration: var(--md-sys-typescale-display-large-text-decoration); |
526 | } |
527 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | :root { |
18 | color-scheme: dark; |
19 | /* Surface tint */ |
20 | --md-sys-color-surface-tint: var(--md-sys-color-primary); |
21 | /* Surface tint color */ |
22 | --md-sys-color-surface-tint-color: var(--md-sys-color-primary); |
23 | /* On error container */ |
24 | --md-sys-color-on-error-container: var(--md-ref-palette-error80); |
25 | /* On error */ |
26 | --md-sys-color-on-error: var(--md-ref-palette-error20); |
27 | /* Error container */ |
28 | --md-sys-color-error-container: var(--md-ref-palette-error30); |
29 | /* On tertiary container */ |
30 | --md-sys-color-on-tertiary-container: var(--md-ref-palette-tertiary90); |
31 | /* On tertiary */ |
32 | --md-sys-color-on-tertiary: var(--md-ref-palette-tertiary20); |
33 | /* Tertiary container */ |
34 | --md-sys-color-tertiary-container: var(--md-ref-palette-tertiary30); |
35 | /* Tertiary */ |
36 | --md-sys-color-tertiary: var(--md-ref-palette-tertiary80); |
37 | /* Shadow */ |
38 | --md-sys-color-shadow: var(--md-ref-palette-neutral0); |
39 | /* Error */ |
40 | --md-sys-color-error: var(--md-ref-palette-error80); |
41 | /* Outline */ |
42 | --md-sys-color-outline: var(--md-ref-palette-neutral-variant60); |
43 | /* On background */ |
44 | --md-sys-color-on-background: var(--md-ref-palette-neutral90); |
45 | /* Background */ |
46 | --md-sys-color-background: var(--md-ref-palette-neutral10); |
47 | /* Inverse on surface */ |
48 | --md-sys-color-inverse-on-surface: var(--md-ref-palette-neutral20); |
49 | /* Inverse surface */ |
50 | --md-sys-color-inverse-surface: var(--md-ref-palette-neutral90); |
51 | /* On surface variant */ |
52 | --md-sys-color-on-surface-variant: var(--md-ref-palette-neutral-variant80); |
53 | /* On surface */ |
54 | --md-sys-color-on-surface: var(--md-ref-palette-neutral90); |
55 | /* Surface Variant */ |
56 | --md-sys-color-surface-variant: var(--md-ref-palette-neutral-variant30); |
57 | /* Surface */ |
58 | --md-sys-color-surface: var(--md-ref-palette-neutral10); |
59 | /* On secondary container */ |
60 | --md-sys-color-on-secondary-container: var(--md-ref-palette-secondary90); |
61 | /* On secondary */ |
62 | --md-sys-color-on-secondary: var(--md-ref-palette-secondary20); |
63 | /* Secondary container */ |
64 | --md-sys-color-secondary-container: var(--md-ref-palette-secondary30); |
65 | /* Secondary */ |
66 | --md-sys-color-secondary: var(--md-ref-palette-secondary80); |
67 | /* Inverse primary */ |
68 | --md-sys-color-inverse-primary: var(--md-ref-palette-primary40); |
69 | /* On primary container */ |
70 | --md-sys-color-on-primary-container: var(--md-ref-palette-primary90); |
71 | /* On primary */ |
72 | --md-sys-color-on-primary: var(--md-ref-palette-primary20); |
73 | /* Primary container */ |
74 | --md-sys-color-primary-container: var(--md-ref-palette-primary30); |
75 | /* Primary */ |
76 | --md-sys-color-primary: var(--md-ref-palette-primary80); |
77 | } |
78 |
1 | /* |
2 | Copyright 2016 Google Inc. All rights reserved. |
3 | |
4 | Licensed under the Apache License, Version 2.0 (the "License"); |
5 | you may not use this file except in compliance with the License. |
6 | You may obtain a copy of the License at |
7 | |
8 | http://www.apache.org/licenses/LICENSE-2.0 |
9 | |
10 | Unless required by applicable law or agreed to in writing, software |
11 | distributed under the License is distributed on an "AS IS" BASIS, |
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | See the License for the specific language governing permissions and |
14 | limitations under the License. |
15 | */ |
16 | |
17 | :root { |
18 | color-scheme: light; |
19 | /* Surface tint */ |
20 | --md-sys-color-surface-tint: var(--md-sys-color-primary); |
21 | /* Surface tint color */ |
22 | --md-sys-color-surface-tint-color: var(--md-sys-color-primary); |
23 | /* On error container */ |
24 | --md-sys-color-on-error-container: var(--md-ref-palette-error10); |
25 | /* On error */ |
26 | --md-sys-color-on-error: var(--md-ref-palette-error100); |
27 | /* Error container */ |
28 | --md-sys-color-error-container: var(--md-ref-palette-error90); |
29 | /* On tertiary container */ |
30 | --md-sys-color-on-tertiary-container: var(--md-ref-palette-tertiary10); |
31 | /* On tertiary */ |
32 | --md-sys-color-on-tertiary: var(--md-ref-palette-tertiary100); |
33 | /* Tertiary container */ |
34 | --md-sys-color-tertiary-container: var(--md-ref-palette-tertiary90); |
35 | /* Tertiary */ |
36 | --md-sys-color-tertiary: var(--md-ref-palette-tertiary40); |
37 | /* Shadow */ |
38 | --md-sys-color-shadow: var(--md-ref-palette-neutral0); |
39 | /* Error */ |
40 | --md-sys-color-error: var(--md-ref-palette-error40); |
41 | /* Outline */ |
42 | --md-sys-color-outline: var(--md-ref-palette-neutral-variant50); |
43 | /* On background */ |
44 | --md-sys-color-on-background: var(--md-ref-palette-neutral10); |
45 | /* Background */ |
46 | --md-sys-color-background: var(--md-ref-palette-neutral99); |
47 | /* Inverse on surface */ |
48 | --md-sys-color-inverse-on-surface: var(--md-ref-palette-neutral95); |
49 | /* Inverse surface */ |
50 | --md-sys-color-inverse-surface: var(--md-ref-palette-neutral20); |
51 | /* On surface variant */ |
52 | --md-sys-color-on-surface-variant: var(--md-ref-palette-neutral-variant30); |
53 | /* On surface */ |
54 | --md-sys-color-on-surface: var(--md-ref-palette-neutral10); |
55 | /* Surface Variant */ |
56 | --md-sys-color-surface-variant: var(--md-ref-palette-neutral-variant90); |
57 | /* Surface */ |
58 | --md-sys-color-surface: var(--md-ref-palette-neutral99); |
59 | /* On secondary container */ |
60 | --md-sys-color-on-secondary-container: var(--md-ref-palette-secondary10); |
61 | /* On secondary */ |
62 | --md-sys-color-on-secondary: var(--md-ref-palette-secondary100); |
63 | /* Secondary container */ |
64 | --md-sys-color-secondary-container: var(--md-ref-palette-secondary90); |
65 | /* Secondary */ |
66 | --md-sys-color-secondary: var(--md-ref-palette-secondary40); |
67 | /* Inverse primary */ |
68 | --md-sys-color-inverse-primary: var(--md-ref-palette-primary80); |
69 | /* On primary container */ |
70 | --md-sys-color-on-primary-container: var(--md-ref-palette-primary10); |
71 | /* On primary */ |
72 | --md-sys-color-on-primary: var(--md-ref-palette-primary100); |
73 | /* Primary container */ |
74 | --md-sys-color-primary-container: var(--md-ref-palette-primary90); |
75 | /* Primary */ |
76 | --md-sys-color-primary: var(--md-ref-palette-primary40); |
77 | } |
78 |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
1 | { |
2 | "compilerOptions": { |
3 | "checkJs": true, |
4 | "strictNullChecks": true, |
5 | "target": "ES6", |
6 | "module": "Node16", |
7 | "moduleResolution": "Node16", |
8 | "lib": [ |
9 | "ES2017", |
10 | "WebWorker", |
11 | "DOM" |
12 | ] |
13 | } |
14 | } |
En esta lección se presentó una PWA con Material Design que incluye:
Diseño de formularios para móviles.
Elementos de interfaces móviles básicas para Material Design con HTML, CSS y JavaScript.
En esta lección se muestra como aceder directamente a algunas funciones del dispositivo.
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" |
8 | content="width=device-width"> |
9 | |
10 | <title>GPS</title> |
11 | |
12 | <style> |
13 | html { |
14 | color-scheme: light dark; |
15 | } |
16 | </style> |
17 | |
18 | </head> |
19 | |
20 | <body> |
21 | |
22 | <h1>GPS</h1> |
23 | |
24 | <p> |
25 | <label> |
26 | Latitud |
27 | <output id="latitud"></output> |
28 | </label> |
29 | </p> |
30 | |
31 | <p> |
32 | <label> |
33 | Longitud |
34 | <output id="longitud"></output> |
35 | </label> |
36 | </p> |
37 | |
38 | <script> |
39 | |
40 | navigator.geolocation. |
41 | watchPosition(p => { |
42 | |
43 | latitud.value = |
44 | p.coords.latitude |
45 | |
46 | longitud.value = |
47 | p.coords.longitude |
48 | |
49 | }); |
50 | |
51 | </script> |
52 | |
53 | </body> |
54 | |
55 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" |
8 | content="width=device-width"> |
9 | |
10 | <title>Archivos y Cámara</title> |
11 | |
12 | <style> |
13 | html { |
14 | color-scheme: light dark; |
15 | } |
16 | </style> |
17 | |
18 | </head> |
19 | |
20 | <body> |
21 | |
22 | <h1>Archivos y Cámara</h1> |
23 | |
24 | <p> |
25 | <label> |
26 | Foto de Móvil |
27 | <input type="file" |
28 | accept="image/*" |
29 | capture="camera"> |
30 | </label> |
31 | </p> |
32 | |
33 | <p> |
34 | <label> |
35 | Video de Móvil |
36 | <input type="file" |
37 | accept="video/*" |
38 | capture=""> |
39 | </label> |
40 | </p> |
41 | |
42 | <p> |
43 | <label> |
44 | Archivo |
45 | <input type="file"> |
46 | </label> |
47 | </p> |
48 | |
49 | </body> |
50 | |
51 | </html> |
1 | <!DOCTYPE html> |
2 | <html> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="utf-8"> |
7 | <meta name="viewport" |
8 | content="width=device-width"> |
9 | |
10 | <title>Cámara</title> |
11 | |
12 | <style> |
13 | html { |
14 | color-scheme: light dark; |
15 | } |
16 | </style> |
17 | |
18 | </head> |
19 | |
20 | <body> |
21 | |
22 | <h1>Cámara</h1> |
23 | |
24 | <p> |
25 | Para grabar o capturar imagen, |
26 | cliquea |
27 | <strong>Inicia</strong>. |
28 | </p> |
29 | |
30 | <p> |
31 | Para grabar por 5 segundos |
32 | cliquea |
33 | <strong>Graba</strong> |
34 | y cliquea |
35 | <strong>Para</strong> para |
36 | detener. |
37 | </p> |
38 | |
39 | <p> |
40 | Para capturar una imagen de la |
41 | cámara, cliquea |
42 | <strong>Captura</strong>. |
43 | </p> |
44 | |
45 | <menu style="display: flex; |
46 | flex-wrap: wrap; |
47 | list-style: none;"> |
48 | <li> |
49 | <button type="button" |
50 | onclick="inicia()"> |
51 | Inicia |
52 | </button> |
53 | </li> |
54 | <li> |
55 | <button type="button" |
56 | onclick="graba()"> |
57 | Graba |
58 | </button> |
59 | </li> |
60 | <li> |
61 | <button type="button" |
62 | onclick="para();"> |
63 | Para |
64 | </button> |
65 | </li> |
66 | <li> |
67 | <button type="button" |
68 | onclick="captura()"> |
69 | Captura |
70 | </button> |
71 | </li> |
72 | </menu> |
73 | |
74 | <section |
75 | style="display: inline-block; |
76 | vertical-align: top;"> |
77 | |
78 | <h1>Preview</h1> |
79 | |
80 | <video id="preview" width="160" |
81 | height="120" autoplay |
82 | muted></video> |
83 | |
84 | </section> |
85 | |
86 | <section |
87 | style="display: inline-block; |
88 | vertical-align: top;"> |
89 | |
90 | <h1>Recording</h1> |
91 | |
92 | <video id="recording" width="160" |
93 | height="120" controls></video> |
94 | |
95 | <p> |
96 | <a id="descarga">Descarga</a> |
97 | </p> |
98 | |
99 | <div id="mensajes"></div> |
100 | |
101 | </section> |
102 | |
103 | <section |
104 | style="display: inline-block; |
105 | vertical-align: top;"> |
106 | |
107 | <h1>Imagen</h1> |
108 | |
109 | <canvas id="canvas" width="160" |
110 | height="120"></canvas> |
111 | |
112 | <p> |
113 | <a id="descargaImagen"> |
114 | Descarga</a> |
115 | </p> |
116 | |
117 | </section> |
118 | |
119 | <script> |
120 | |
121 | let stream = null |
122 | |
123 | let TIEMPO_DE_GRABACION = 5000 |
124 | |
125 | var context = |
126 | canvas.getContext('2d') |
127 | |
128 | async function inicia() { |
129 | try { |
130 | stream = await navigator |
131 | .mediaDevices.getUserMedia({ |
132 | video: true, |
133 | audio: true |
134 | }) |
135 | preview.srcObject = stream |
136 | descarga.href = stream |
137 | preview.captureStream = |
138 | preview.captureStream |
139 | || preview.mozCaptureStream |
140 | await new Promise( |
141 | resolve => |
142 | preview.onplaying = resolve) |
143 | } catch (e) { |
144 | log(e.message) |
145 | } |
146 | } |
147 | |
148 | async function graba() { |
149 | try { |
150 | const recordedChunks = |
151 | await grabaClip(stream, |
152 | TIEMPO_DE_GRABACION) |
153 | let recordedBlob = new Blob( |
154 | recordedChunks, |
155 | { type: "video/webm" }) |
156 | recording.src = |
157 | URL.createObjectURL( |
158 | recordedBlob) |
159 | descarga.href = recording.src |
160 | descarga.download = |
161 | "RecordedVideo.webm" |
162 | |
163 | log("Exitosamente grabados " |
164 | + recordedBlob.size |
165 | + " bytes de " |
166 | + recordedBlob.type |
167 | + " media.") |
168 | } catch (e) { |
169 | log(e.message) |
170 | } |
171 | } |
172 | |
173 | function para() { |
174 | detiene(preview.srcObject) |
175 | } |
176 | |
177 | function captura() { |
178 | context.drawImage(preview, |
179 | 0, 0, 160, 120) |
180 | descargaImagen.href = |
181 | canvas.toDataURL('image/jpeg') |
182 | descargaImagen.download = |
183 | "imagen.jpg" |
184 | } |
185 | |
186 | function grabaClip(stream, |
187 | milisegundos) { |
188 | let recorder = |
189 | new MediaRecorder(stream) |
190 | let data = [] |
191 | recorder.ondataavailable = |
192 | event => data.push(event.data) |
193 | recorder.start() |
194 | log(recorder.state |
195 | + " durante " |
196 | + (milisegundos / 1000) |
197 | + " segundos…") |
198 | let detenido = new Promise( |
199 | (resolve, reject) => { |
200 | recorder.onstop = resolve |
201 | recorder.onerror = |
202 | event => reject(event.name) |
203 | }) |
204 | let grabado = |
205 | espera(milisegundos) |
206 | .then(() => recorder.state |
207 | === "recording" |
208 | && recorder.stop() |
209 | ) |
210 | |
211 | return Promise.all([ |
212 | detenido, |
213 | grabado |
214 | ]) |
215 | .then(() => data) |
216 | } |
217 | |
218 | function detiene(stream) { |
219 | stream.getTracks().forEach( |
220 | track => track.stop()) |
221 | } |
222 | |
223 | function log(msg) { |
224 | mensajes.innerHTML += |
225 | msg + "<br>" |
226 | } |
227 | |
228 | function espera(milisegundos) { |
229 | return new Promise( |
230 | resolve => setTimeout( |
231 | resolve, milisegundos)) |
232 | } |
233 | |
234 | </script> |
235 | |
236 | </body> |
237 | |
238 | </html> |
En esta lección se revisaron:
Acceso al GPS.
Acceso a la cámara.
Acceso a los archivos.
En esta lección se presenta el ejemplo base para IoT.
Puedes probar el ejemplo en https://iothtml.rf.gd/.
Prueba el ejemplo en https://iothtml.rf.gd/.
Copia la url de la app y pégala en varias pestañas, navegadores y dispositivos para que veas como entre todas estas vistas se puede chatear.
Este proyecto puede correr simultáneamente en varios navegadores y computadoras. Todos interactuan con el servidor test.mosquitto.org.
Este ejercicio usa la librería Eclipse Paho JavaScript Client para conectar el JavaScript del navegador web para conectarse al servidor de mqtt. Puedes profundizar en este tema en la URL https://eclipse.dev/paho/clients/js/
Descarga el archivo /src/iothtml.zip y descompáctalo.
Crea tu proyecto en GitHub pages:
Crea una cuenta de email con el nombre de tu sitio, por ejemplo, miapp@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo miapp.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *. Debes usar el nombre de tu cuenta seguido por .github.io; por ejemplo miapp.github.io
Mantén la selección Public.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Entra al repositorio y selecciona ⚙ Settings, luego selecciona 📁 Pages y en la sección Branches selecciona la carpeta donde se ubicará la carpeta. De preferencia selecciona / (root) para que coloques la página en la raíz del proyecto.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del
ejemplo, excepto el archivo .htaccess
.
Edita los archivos que desees.
Si tu proyecto no usa backend, haz clic derecho en
index.html
, selecciona Open with Live Server y se abre el
navegador para que puedas probar localmente el ejemplo.
Si tu proyecto usa PHP, haz clic derecho en
index.html
, selecciona PHP Server: serve project y se abre
el navegador para que puedas probar localmente el ejemplo.
Para depurar paso a paso haz lo siguiente:
En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.
Recarga la página, de preferencia haciendo clic derecho en el ícono de volver a cargar la página y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página . Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.
Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).
Selecciona el archivo donde vas a empezar a depurar.
Haz clic en el número de la línea donde vas a empezar a depurar.
Recarga la página de manera normal.
Empieza a usar tu sitio.
Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Si usas GitHub pages:
Entra a la página de tu repositorio y abajo a la derecha, selecciona el enlace github-pages.
Se muestran los despliegues realizados. Recarga la página hasta que apareca el mensaje de tu último push con una palomita dentro de un círculo verde.
Los archivos duran 10 minutos en la caché del navegador. Para ver los cambios antes, borra el historial y recarga la página.
Si no usas GitHub pages:
Sube el proyecto al hosting que elijas sin incluir el archivo
.htaccess
. En algunos casos puedes usar
filezilla
(https://filezilla-project.org/)
En algunos host como InfinityFree, tienes que configurar el certificado SSL.
En algunos host como InfinityFree, debes subir el
archivo .htaccess
cuando el certificado SSL se ha creado e
instalado. Sirve para forzar el uso de https.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Haz clic en los triángulos para expandir las carpetas
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="utf-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>IoT</title> |
10 | |
11 | <script src="paho.javascript-1.0.3/paho-mqtt-min.js"></script> |
12 | |
13 | </head> |
14 | |
15 | <body> |
16 | |
17 | <h1>IoT</h1> |
18 | |
19 | <p> |
20 | <output id="salida"> |
21 | <progress max="100">Cargando…</progress> |
22 | </output> |
23 | </p> |
24 | |
25 | <p> |
26 | <button type="button" onclick="clicEnBoton()"> |
27 | Enviar |
28 | </button> |
29 | </p> |
30 | |
31 | <script type="module"> |
32 | |
33 | import { exportaAHtml } from "./lib/js/exportaAHtml.js" |
34 | import { creaIdCliente } from "./lib/js/creaIdCliente.js" |
35 | import { falloEnLaConexionMqtt } from "./lib/js/falloEnLaConexionMqtt.js" |
36 | import { conexionMqttPerdida } from "./lib/js/conexionMqttPerdida.js" |
37 | import { muestraError } from "./lib/js/muestraError.js" |
38 | |
39 | // A cada control le corresponde un tópico diferente. |
40 | const TOPICO_FOCO = "gilpgdm/IoT/foco" |
41 | |
42 | let valor = "0" |
43 | |
44 | // Cambia por una raíz para tu proyecto. |
45 | const clientId = creaIdCliente("gilpgdmIoT-") |
46 | |
47 | // Si usas un servidor de MQTT diferente, necesitas cambiar los parámetros. |
48 | const cliente = new Paho.MQTT.Client("test.mosquitto.org", 8081, clientId) |
49 | |
50 | function clicEnBoton() { |
51 | try { |
52 | enviaMensajeMqtt(valor === '1' ? '0' : '1', TOPICO_FOCO) |
53 | } catch (error) { |
54 | muestraError(error) |
55 | } |
56 | } |
57 | exportaAHtml(clicEnBoton) |
58 | |
59 | // Acciones al recibir un mensaje. |
60 | cliente.onMessageArrived = mensaje => { |
61 | if (mensaje.destinationName === TOPICO_FOCO) { |
62 | valor = mensaje.payloadString |
63 | salida.value = valor === "1" ? "🔴" : "⚪" |
64 | } |
65 | } |
66 | |
67 | // Acciones al perder la conexión. |
68 | cliente.onConnectionLost = conexionMqttPerdida |
69 | |
70 | // Configura el cliente. |
71 | cliente.connect({ |
72 | |
73 | keepAliveInterval: 10, |
74 | |
75 | useSSL: true, |
76 | |
77 | // Acciones al fallar la conexión. |
78 | onFailure: falloEnLaConexionMqtt, |
79 | |
80 | // Acciones al lograr la conexión. |
81 | onSuccess: () => { |
82 | console.log("Conectado") |
83 | // Se suscribe a uno o más tópicos. |
84 | cliente.subscribe(TOPICO_FOCO) |
85 | // Envía el valor inicial al tópico. |
86 | enviaMensajeMqtt(valor, TOPICO_FOCO) |
87 | }, |
88 | |
89 | }) |
90 | |
91 | /** |
92 | * Envá un valor al servidor de MQTT y es reenviado a todos los dispositivos |
93 | * suscritos al tópico indicado |
94 | * @param {string} mensaje |
95 | * @param {string} topico |
96 | */ |
97 | function enviaMensajeMqtt(mensaje, topico) { |
98 | const mensajeMqtt = new Paho.MQTT.Message(mensaje) |
99 | mensajeMqtt.destinationName = topico |
100 | cliente.send(mensajeMqtt) |
101 | } |
102 | |
103 | </script> |
104 | |
105 | </body> |
106 | |
107 | </html> |
1 | /** |
2 | * @param { { |
3 | * errorCode: number, |
4 | * errorMessage: string |
5 | * } } responseObject |
6 | */ |
7 | export function conexionMqttPerdida(responseObject) { |
8 | if (responseObject.errorCode !== 0) { |
9 | const mensaje = "Conexión terminada " + responseObject.errorMessage |
10 | console.error(mensaje) |
11 | alert(mensaje) |
12 | } |
13 | } |
1 | /** |
2 | * Añade caracteres al azar a una raíz, para obtener un clientId único. |
3 | * @param {string} raiz |
4 | */ |
5 | export function creaIdCliente(raiz) { |
6 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
7 | for (var i = 0; i < 15; i++) { |
8 | raiz += chars.charAt(Math.floor(Math.random() * chars.length)) |
9 | } |
10 | return raiz |
11 | } |
1 | /** |
2 | * Permite que los eventos de html usen la función. |
3 | * @param {function} functionInstance |
4 | */ |
5 | export function exportaAHtml(functionInstance) { |
6 | window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance |
7 | } |
8 | |
9 | /** |
10 | * @param {function} valor |
11 | */ |
12 | export function nombreDeFuncionParaHtml(valor) { |
13 | const names = valor.name.split(/\s+/g) |
14 | return names[names.length - 1] |
15 | } |
1 | /** |
2 | * @param { { errorMessage: string } } res |
3 | */ |
4 | export function falloEnLaConexionMqtt(res) { |
5 | const mensaje = "Fallo en conexión:" + res.errorMessage |
6 | console.error(mensaje) |
7 | alert(mensaje) |
8 | } |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Muestra un error en la consola y en un cuadro de |
6 | * alerta el mensaje de una excepción. |
7 | * @param { ProblemDetails | Error | null } error descripción del error. |
8 | */ |
9 | export function muestraError(error) { |
10 | |
11 | if (error === null) { |
12 | |
13 | console.error("Error") |
14 | alert("Error") |
15 | |
16 | } else if (error instanceof ProblemDetails) { |
17 | |
18 | let mensaje = error.title |
19 | if (error.detail) { |
20 | mensaje += `\n\n${error.detail}` |
21 | } |
22 | mensaje += `\n\nCódigo: ${error.status}` |
23 | if (error.type) { |
24 | mensaje += ` ${error.type}` |
25 | } |
26 | |
27 | console.error(mensaje) |
28 | console.error(error) |
29 | console.error("Headers:") |
30 | error.headers.forEach((valor, llave) => console.error(llave, "=", valor)) |
31 | alert(mensaje) |
32 | |
33 | } else { |
34 | |
35 | console.error(error) |
36 | alert(error.message) |
37 | |
38 | } |
39 | |
40 | } |
41 | |
42 | exportaAHtml(muestraError) |
1 | /** |
2 | * Detalle de los errores devueltos por un servicio. |
3 | */ |
4 | export class ProblemDetails extends Error { |
5 | |
6 | /** |
7 | * @param {number} status |
8 | * @param {Headers} headers |
9 | * @param {string} title |
10 | * @param {string} [type] |
11 | * @param {string} [detail] |
12 | */ |
13 | constructor(status, headers, title, type, detail) { |
14 | super(title) |
15 | /** |
16 | * @readonly |
17 | */ |
18 | this.status = status |
19 | /** |
20 | * @readonly |
21 | */ |
22 | this.headers = headers |
23 | /** |
24 | * @readonly |
25 | */ |
26 | this.type = type |
27 | /** |
28 | * @readonly |
29 | */ |
30 | this.detail = detail |
31 | /** |
32 | * @readonly |
33 | */ |
34 | this.title = title |
35 | } |
36 | |
37 | } |
1 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> |
2 | <html xmlns="http://www.w3.org/1999/xhtml"><head> |
3 | <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> |
4 | <title>About</title> |
5 | </head> |
6 | <body lang="EN-US"> |
7 | <h2>About This Content</h2> |
8 | |
9 | <p><em>December 9, 2013</em></p> |
10 | <h3>License</h3> |
11 | |
12 | <p>The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise |
13 | indicated below, the Content is provided to you under the terms and conditions of the |
14 | Eclipse Public License Version 1.0 ("EPL") and Eclipse Distribution License Version 1.0 ("EDL"). |
15 | A copy of the EPL is available at |
16 | <a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a> |
17 | and a copy of the EDL is available at |
18 | <a href="http://www.eclipse.org/org/documents/edl-v10.php">http://www.eclipse.org/org/documents/edl-v10.php</a>. |
19 | For purposes of the EPL, "Program" will mean the Content.</p> |
20 | |
21 | <p>If you did not receive this Content directly from the Eclipse Foundation, the Content is |
22 | being redistributed by another party ("Redistributor") and different terms and conditions may |
23 | apply to your use of any object code in the Content. Check the Redistributor's license that was |
24 | provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise |
25 | indicated below, the terms and conditions of the EPL still apply to any source code in the Content |
26 | and such source code may be obtained at <a href="http://www.eclipse.org/">http://www.eclipse.org</a>.</p> |
27 | |
28 | </body></html> |
29 |
1 | # Contributing to Paho |
2 | |
3 | Thanks for your interest in this project! |
4 | |
5 | You can contribute bugfixes and new features by sending pull requests through GitHub. |
6 | |
7 | ## Legal |
8 | |
9 | In order for your contribution to be accepted, it must comply with the Eclipse Foundation IP policy. |
10 | |
11 | Please read the [Eclipse Foundation policy on accepting contributions via Git](http://wiki.eclipse.org/Development_Resources/Contributing_via_Git). |
12 | |
13 | 1. Sign the [Eclipse CLA](http://www.eclipse.org/legal/CLA.php) |
14 | 1. Register for an Eclipse Foundation User ID. You can register [here](https://dev.eclipse.org/site_login/createaccount.php). |
15 | 2. Log into the [Projects Portal](https://projects.eclipse.org/), and click on the '[Eclipse CLA](https://projects.eclipse.org/user/sign/cla)' link. |
16 | 2. Go to your [account settings](https://dev.eclipse.org/site_login/myaccount.php#open_tab_accountsettings) and add your GitHub username to your account. |
17 | 3. Make sure that you _sign-off_ your Git commits in the following format: |
18 | ``` Signed-off-by: John Smith |
19 | 4. Ensure that the email address that you make your commits with is the same one you used to sign up to the Eclipse Foundation website with. |
20 | |
21 | ## Contributing a change |
22 | |
23 | ## Contributing a change |
24 | |
25 | 1. [Fork the repository on GitHub](https://github.com/eclipse/paho.mqtt.javascript/fork) |
26 | 2. Clone the forked repository onto your computer: ``` git clone https://github.com/ |
27 | 3. Create a new branch from the latest ```develop``` branch with ```git checkout -b YOUR_BRANCH_NAME origin/develop``` |
28 | 4. Make your changes |
29 | 5. If developing a new feature, make sure to include JUnit tests. |
30 | 6. Ensure that all new and existing tests pass. |
31 | 7. Commit the changes into the branch: ``` git commit -s ``` Make sure that your commit message is meaningful and describes your changes correctly. |
32 | 8. If you have a lot of commits for the change, squash them into a single / few commits. |
33 | 9. Push the changes in your branch to your forked repository. |
34 | 10. Finally, go to [https://github.com/eclipse/paho.mqtt.javascript](https://github.com/eclipse/paho.mqtt.javascript) and create a pull request from your "YOUR_BRANCH_NAME" branch to the ```develop``` one to request review and merge of the commits in your pushed branch. |
35 | |
36 | |
37 | What happens next depends on the content of the patch. If it is 100% authored |
38 | by the contributor and is less than 1000 lines (and meets the needs of the |
39 | project), then it can be pulled into the main repository. If not, more steps |
40 | are required. These are detailed in the |
41 | [legal process poster](http://www.eclipse.org/legal/EclipseLegalProcessPoster.pdf). |
42 | |
43 | |
44 | |
45 | ## Developer resources: |
46 | |
47 | |
48 | Information regarding source code management, builds, coding standards, and more. |
49 | |
50 | - [https://projects.eclipse.org/projects/iot.paho/developer](https://projects.eclipse.org/projects/iot.paho/developer) |
51 | |
52 | Contact: |
53 | -------- |
54 | |
55 | Contact the project developers via the project's development |
56 | [mailing list](https://dev.eclipse.org/mailman/listinfo/paho-dev). |
57 | |
58 | Search for bugs: |
59 | ---------------- |
60 | |
61 | This project uses GitHub Issues here: [github.com/eclipse/paho.mqtt.javascript/issues](https://github.com/eclipse/paho.mqtt.javascript/issues) to track ongoing development and issues. |
62 | |
63 | Create a new bug: |
64 | ----------------- |
65 | |
66 | Be sure to search for existing bugs before you create another one. Remember that contributions are always welcome! |
67 | |
68 | - [Create new Paho bug](https://github.com/eclipse/paho.mqtt.javascript/issues/new) |
69 |
1 | |
2 | Eclipse Distribution License - v 1.0 |
3 | |
4 | Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. |
5 | |
6 | All rights reserved. |
7 | |
8 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: |
9 | |
10 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. |
11 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. |
12 | Neither the name of the Eclipse Foundation, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. |
13 | |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
15 | |
16 |
1 | Eclipse Public License - v 1.0 |
2 | |
3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. |
4 | |
5 | 1. DEFINITIONS |
6 | |
7 | "Contribution" means: |
8 | |
9 | a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and |
10 | b) in the case of each subsequent Contributor: |
11 | i) changes to the Program, and |
12 | ii) additions to the Program; |
13 | where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. |
14 | "Contributor" means any person or entity that distributes the Program. |
15 | |
16 | "Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. |
17 | |
18 | "Program" means the Contributions distributed in accordance with this Agreement. |
19 | |
20 | "Recipient" means anyone who receives the Program under this Agreement, including all Contributors. |
21 | |
22 | 2. GRANT OF RIGHTS |
23 | |
24 | a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. |
25 | b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. |
26 | c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. |
27 | d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. |
28 | 3. REQUIREMENTS |
29 | |
30 | A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: |
31 | |
32 | a) it complies with the terms and conditions of this Agreement; and |
33 | b) its license agreement: |
34 | i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; |
35 | ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; |
36 | iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and |
37 | iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. |
38 | When the Program is made available in source code form: |
39 | |
40 | a) it must be made available under this Agreement; and |
41 | b) a copy of this Agreement must be included with each copy of the Program. |
42 | Contributors may not remove or alter any copyright notices contained within the Program. |
43 | |
44 | Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. |
45 | |
46 | 4. COMMERCIAL DISTRIBUTION |
47 | |
48 | Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. |
49 | |
50 | For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. |
51 | |
52 | 5. NO WARRANTY |
53 | |
54 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED 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. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. |
55 | |
56 | 6. DISCLAIMER OF LIABILITY |
57 | |
58 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. |
59 | |
60 | 7. GENERAL |
61 | |
62 | If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. |
63 | |
64 | If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. |
65 | |
66 | All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. |
67 | |
68 | Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. |
69 | |
70 | This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. |
71 |
1 | /******************************************************************************* |
2 | * Copyright (c) 2013, 2016 IBM Corp. |
3 | * |
4 | * All rights reserved. This program and the accompanying materials |
5 | * are made available under the terms of the Eclipse Public License v1.0 |
6 | * and Eclipse Distribution License v1.0 which accompany this distribution. |
7 | * |
8 | * The Eclipse Public License is available at |
9 | * http://www.eclipse.org/legal/epl-v10.html |
10 | * and the Eclipse Distribution License is available at |
11 | * http://www.eclipse.org/org/documents/edl-v10.php. |
12 | * |
13 | *******************************************************************************/ |
14 | (function(p,s){"object"===typeof exports&&"object"===typeof module?module.exports=s():"function"===typeof define&&define.amd?define(s):"object"===typeof exports?exports=s():("undefined"===typeof p.Paho&&(p.Paho={}),p.Paho.MQTT=s())})(this,function(){return function(p){function s(a,b,c){b[c++]=a>>8;b[c++]=a%256;return c}function u(a,b,c,k){k=s(b,c,k);D(a,c,k);return k+b}function n(a){for(var b=0,c=0;c<a.length;c++){var k=a.charCodeAt(c);2047<k?(55296<=k&&56319>=k&&(c++,b++),b+=3):127<k?b+=2:b++}return b} |
15 | function D(a,b,c){for(var k=0;k<a.length;k++){var e=a.charCodeAt(k);if(55296<=e&&56319>=e){var g=a.charCodeAt(++k);if(isNaN(g))throw Error(f(h.MALFORMED_UNICODE,[e,g]));e=(e-55296<<10)+(g-56320)+65536}127>=e?b[c++]=e:(2047>=e?b[c++]=e>>6&31|192:(65535>=e?b[c++]=e>>12&15|224:(b[c++]=e>>18&7|240,b[c++]=e>>12&63|128),b[c++]=e>>6&63|128),b[c++]=e&63|128)}return b}function E(a,b,c){for(var k="",e,g=b;g<b+c;){e=a[g++];if(!(128>e)){var m=a[g++]-128;if(0>m)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16), |
16 | ""]));if(224>e)e=64*(e-192)+m;else{var d=a[g++]-128;if(0>d)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16)]));if(240>e)e=4096*(e-224)+64*m+d;else{var l=a[g++]-128;if(0>l)throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16),l.toString(16)]));if(248>e)e=262144*(e-240)+4096*m+64*d+l;else throw Error(f(h.MALFORMED_UTF,[e.toString(16),m.toString(16),d.toString(16),l.toString(16)]));}}}65535<e&&(e-=65536,k+=String.fromCharCode(55296+(e>>10)),e=56320+(e& |
17 | 1023));k+=String.fromCharCode(e)}return k}var z=function(a,b){for(var c in a)if(a.hasOwnProperty(c))if(b.hasOwnProperty(c)){if(typeof a[c]!==b[c])throw Error(f(h.INVALID_TYPE,[typeof a[c],c]));}else{c="Unknown property, "+c+". Valid properties are:";for(var k in b)b.hasOwnProperty(k)&&(c=c+" "+k);throw Error(c);}},v=function(a,b){return function(){return a.apply(b,arguments)}},h={OK:{code:0,text:"AMQJSC0000I OK."},CONNECT_TIMEOUT:{code:1,text:"AMQJSC0001E Connect timed out."},SUBSCRIBE_TIMEOUT:{code:2, |
18 | text:"AMQJS0002E Subscribe timed out."},UNSUBSCRIBE_TIMEOUT:{code:3,text:"AMQJS0003E Unsubscribe timed out."},PING_TIMEOUT:{code:4,text:"AMQJS0004E Ping timed out."},INTERNAL_ERROR:{code:5,text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"},CONNACK_RETURNCODE:{code:6,text:"AMQJS0006E Bad Connack return code:{0} {1}."},SOCKET_ERROR:{code:7,text:"AMQJS0007E Socket error:{0}."},SOCKET_CLOSE:{code:8,text:"AMQJS0008I Socket closed."},MALFORMED_UTF:{code:9,text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."}, |
19 | UNSUPPORTED:{code:10,text:"AMQJS0010E {0} is not supported by this browser."},INVALID_STATE:{code:11,text:"AMQJS0011E Invalid state {0}."},INVALID_TYPE:{code:12,text:"AMQJS0012E Invalid type {0} for {1}."},INVALID_ARGUMENT:{code:13,text:"AMQJS0013E Invalid argument {0} for {1}."},UNSUPPORTED_OPERATION:{code:14,text:"AMQJS0014E Unsupported operation."},INVALID_STORED_DATA:{code:15,text:"AMQJS0015E Invalid data in local storage key\x3d{0} value\x3d{1}."},INVALID_MQTT_MESSAGE_TYPE:{code:16,text:"AMQJS0016E Invalid MQTT message type {0}."}, |
20 | MALFORMED_UNICODE:{code:17,text:"AMQJS0017E Malformed Unicode string:{0} {1}."},BUFFER_FULL:{code:18,text:"AMQJS0018E Message buffer is full, maximum buffer size: {0}."}},H={0:"Connection Accepted",1:"Connection Refused: unacceptable protocol version",2:"Connection Refused: identifier rejected",3:"Connection Refused: server unavailable",4:"Connection Refused: bad user name or password",5:"Connection Refused: not authorized"},f=function(a,b){var c=a.text;if(b)for(var k,e,g=0;g<b.length;g++)if(k="{"+ |
21 | g+"}",e=c.indexOf(k),0<e)var h=c.substring(0,e),c=c.substring(e+k.length),c=h+b[g]+c;return c},A=[0,6,77,81,73,115,100,112,3],B=[0,4,77,81,84,84,4],q=function(a,b){this.type=a;for(var c in b)b.hasOwnProperty(c)&&(this[c]=b[c])};q.prototype.encode=function(){var a=(this.type&15)<<4,b=0,c=[],k=0,e;void 0!==this.messageIdentifier&&(b+=2);switch(this.type){case 1:switch(this.mqttVersion){case 3:b+=A.length+3;break;case 4:b+=B.length+3}b+=n(this.clientId)+2;void 0!==this.willMessage&&(b+=n(this.willMessage.destinationName)+ |
22 | 2,e=this.willMessage.payloadBytes,e instanceof Uint8Array||(e=new Uint8Array(h)),b+=e.byteLength+2);void 0!==this.userName&&(b+=n(this.userName)+2);void 0!==this.password&&(b+=n(this.password)+2);break;case 8:for(var a=a|2,g=0;g<this.topics.length;g++)c[g]=n(this.topics[g]),b+=c[g]+2;b+=this.requestedQos.length;break;case 10:a|=2;for(g=0;g<this.topics.length;g++)c[g]=n(this.topics[g]),b+=c[g]+2;break;case 6:a|=2;break;case 3:this.payloadMessage.duplicate&&(a|=8);a=a|=this.payloadMessage.qos<<1;this.payloadMessage.retained&& |
23 | (a|=1);var k=n(this.payloadMessage.destinationName),h=this.payloadMessage.payloadBytes,b=b+(k+2)+h.byteLength;h instanceof ArrayBuffer?h=new Uint8Array(h):h instanceof Uint8Array||(h=new Uint8Array(h.buffer))}var f=b,g=Array(1),d=0;do{var t=f%128,f=f>>7;0<f&&(t|=128);g[d++]=t}while(0<f&&4>d);f=g.length+1;b=new ArrayBuffer(b+f);d=new Uint8Array(b);d[0]=a;d.set(g,1);if(3==this.type)f=u(this.payloadMessage.destinationName,k,d,f);else if(1==this.type){switch(this.mqttVersion){case 3:d.set(A,f);f+=A.length; |
24 | break;case 4:d.set(B,f),f+=B.length}a=0;this.cleanSession&&(a=2);void 0!==this.willMessage&&(a=a|4|this.willMessage.qos<<3,this.willMessage.retained&&(a|=32));void 0!==this.userName&&(a|=128);void 0!==this.password&&(a|=64);d[f++]=a;f=s(this.keepAliveInterval,d,f)}void 0!==this.messageIdentifier&&(f=s(this.messageIdentifier,d,f));switch(this.type){case 1:f=u(this.clientId,n(this.clientId),d,f);void 0!==this.willMessage&&(f=u(this.willMessage.destinationName,n(this.willMessage.destinationName),d,f), |
25 | f=s(e.byteLength,d,f),d.set(e,f),f+=e.byteLength);void 0!==this.userName&&(f=u(this.userName,n(this.userName),d,f));void 0!==this.password&&u(this.password,n(this.password),d,f);break;case 3:d.set(h,f);break;case 8:for(g=0;g<this.topics.length;g++)f=u(this.topics[g],c[g],d,f),d[f++]=this.requestedQos[g];break;case 10:for(g=0;g<this.topics.length;g++)f=u(this.topics[g],c[g],d,f)}return b};var F=function(a,b,c){this._client=a;this._window=b;this._keepAliveInterval=1E3*c;this.isReset=!1;var k=(new q(12)).encode(), |
26 | e=function(a){return function(){return g.apply(a)}},g=function(){this.isReset?(this.isReset=!1,this._client._trace("Pinger.doPing","send PINGREQ"),this._client.socket.send(k),this.timeout=this._window.setTimeout(e(this),this._keepAliveInterval)):(this._client._trace("Pinger.doPing","Timed out"),this._client._disconnected(h.PING_TIMEOUT.code,f(h.PING_TIMEOUT)))};this.reset=function(){this.isReset=!0;this._window.clearTimeout(this.timeout);0<this._keepAliveInterval&&(this.timeout=setTimeout(e(this), |
27 | this._keepAliveInterval))};this.cancel=function(){this._window.clearTimeout(this.timeout)}},w=function(a,b,c,f,e){this._window=b;c||(c=30);this.timeout=setTimeout(function(a,b,c){return function(){return a.apply(b,c)}}(f,a,e),1E3*c);this.cancel=function(){this._window.clearTimeout(this.timeout)}},d=function(a,b,c,d,e){if(!("WebSocket"in p&&null!==p.WebSocket))throw Error(f(h.UNSUPPORTED,["WebSocket"]));if(!("localStorage"in p&&null!==p.localStorage))throw Error(f(h.UNSUPPORTED,["localStorage"])); |
28 | if(!("ArrayBuffer"in p&&null!==p.ArrayBuffer))throw Error(f(h.UNSUPPORTED,["ArrayBuffer"]));this._trace("Paho.MQTT.Client",a,b,c,d,e);this.host=b;this.port=c;this.path=d;this.uri=a;this.clientId=e;this._wsuri=null;this._localKey=b+":"+c+("/mqtt"!=d?":"+d:"")+":"+e+":";this._msg_queue=[];this._buffered_msg_queue=[];this._sentMessages={};this._receivedMessages={};this._notify_msg_sent={};this._message_identifier=1;this._sequence=0;for(var g in localStorage)0!==g.indexOf("Sent:"+this._localKey)&&0!== |
29 | g.indexOf("Received:"+this._localKey)||this.restore(g)};d.prototype.host=null;d.prototype.port=null;d.prototype.path=null;d.prototype.uri=null;d.prototype.clientId=null;d.prototype.socket=null;d.prototype.connected=!1;d.prototype.maxMessageIdentifier=65536;d.prototype.connectOptions=null;d.prototype.hostIndex=null;d.prototype.onConnected=null;d.prototype.onConnectionLost=null;d.prototype.onMessageDelivered=null;d.prototype.onMessageArrived=null;d.prototype.traceFunction=null;d.prototype._msg_queue= |
30 | null;d.prototype._buffered_msg_queue=null;d.prototype._connectTimeout=null;d.prototype.sendPinger=null;d.prototype.receivePinger=null;d.prototype._reconnectInterval=1;d.prototype._reconnecting=!1;d.prototype._reconnectTimeout=null;d.prototype.disconnectedPublishing=!1;d.prototype.disconnectedBufferSize=5E3;d.prototype.receiveBuffer=null;d.prototype._traceBuffer=null;d.prototype._MAX_TRACE_ENTRIES=100;d.prototype.connect=function(a){var b=this._traceMask(a,"password");this._trace("Client.connect", |
31 | b,this.socket,this.connected);if(this.connected)throw Error(f(h.INVALID_STATE,["already connected"]));if(this.socket)throw Error(f(h.INVALID_STATE,["already connected"]));this._reconnecting&&(this._reconnectTimeout.cancel(),this._reconnectTimeout=null,this._reconnecting=!1);this.connectOptions=a;this._reconnectInterval=1;this._reconnecting=!1;a.uris?(this.hostIndex=0,this._doConnect(a.uris[0])):this._doConnect(this.uri)};d.prototype.subscribe=function(a,b){this._trace("Client.subscribe",a,b);if(!this.connected)throw Error(f(h.INVALID_STATE, |
32 | ["not connected"]));var c=new q(8);c.topics=[a];c.requestedQos=void 0!==b.qos?[b.qos]:[0];b.onSuccess&&(c.onSuccess=function(a){b.onSuccess({invocationContext:b.invocationContext,grantedQos:a})});b.onFailure&&(c.onFailure=function(a){b.onFailure({invocationContext:b.invocationContext,errorCode:a,errorMessage:f(a)})});b.timeout&&(c.timeOut=new w(this,window,b.timeout,b.onFailure,[{invocationContext:b.invocationContext,errorCode:h.SUBSCRIBE_TIMEOUT.code,errorMessage:f(h.SUBSCRIBE_TIMEOUT)}]));this._requires_ack(c); |
33 | this._schedule_message(c)};d.prototype.unsubscribe=function(a,b){this._trace("Client.unsubscribe",a,b);if(!this.connected)throw Error(f(h.INVALID_STATE,["not connected"]));var c=new q(10);c.topics=[a];b.onSuccess&&(c.callback=function(){b.onSuccess({invocationContext:b.invocationContext})});b.timeout&&(c.timeOut=new w(this,window,b.timeout,b.onFailure,[{invocationContext:b.invocationContext,errorCode:h.UNSUBSCRIBE_TIMEOUT.code,errorMessage:f(h.UNSUBSCRIBE_TIMEOUT)}]));this._requires_ack(c);this._schedule_message(c)}; |
34 | d.prototype.send=function(a){this._trace("Client.send",a);wireMessage=new q(3);wireMessage.payloadMessage=a;if(this.connected)0<a.qos?this._requires_ack(wireMessage):this.onMessageDelivered&&(this._notify_msg_sent[wireMessage]=this.onMessageDelivered(wireMessage.payloadMessage)),this._schedule_message(wireMessage);else if(this._reconnecting&&this.disconnectedPublishing){if(Object.keys(this._sentMessages).length+this._buffered_msg_queue.length>this.disconnectedBufferSize)throw Error(f(h.BUFFER_FULL, |
35 | [this.disconnectedBufferSize]));0<a.qos?this._requires_ack(wireMessage):(wireMessage.sequence=++this._sequence,this._buffered_msg_queue.push(wireMessage))}else throw Error(f(h.INVALID_STATE,["not connected"]));};d.prototype.disconnect=function(){this._trace("Client.disconnect");this._reconnecting&&(this._reconnectTimeout.cancel(),this._reconnectTimeout=null,this._reconnecting=!1);if(!this.socket)throw Error(f(h.INVALID_STATE,["not connecting or connected"]));wireMessage=new q(14);this._notify_msg_sent[wireMessage]= |
36 | v(this._disconnected,this);this._schedule_message(wireMessage)};d.prototype.getTraceLog=function(){if(null!==this._traceBuffer){this._trace("Client.getTraceLog",new Date);this._trace("Client.getTraceLog in flight messages",this._sentMessages.length);for(var a in this._sentMessages)this._trace("_sentMessages ",a,this._sentMessages[a]);for(a in this._receivedMessages)this._trace("_receivedMessages ",a,this._receivedMessages[a]);return this._traceBuffer}};d.prototype.startTrace=function(){null===this._traceBuffer&& |
37 | (this._traceBuffer=[]);this._trace("Client.startTrace",new Date,"1.0.3")};d.prototype.stopTrace=function(){delete this._traceBuffer};d.prototype._doConnect=function(a){this.connectOptions.useSSL&&(a=a.split(":"),a[0]="wss",a=a.join(":"));this._wsuri=a;this.connected=!1;this.socket=4>this.connectOptions.mqttVersion?new WebSocket(a,["mqttv3.1"]):new WebSocket(a,["mqtt"]);this.socket.binaryType="arraybuffer";this.socket.onopen=v(this._on_socket_open,this);this.socket.onmessage=v(this._on_socket_message, |
38 | this);this.socket.onerror=v(this._on_socket_error,this);this.socket.onclose=v(this._on_socket_close,this);this.sendPinger=new F(this,window,this.connectOptions.keepAliveInterval);this.receivePinger=new F(this,window,this.connectOptions.keepAliveInterval);this._connectTimeout&&(this._connectTimeout.cancel(),this._connectTimeout=null);this._connectTimeout=new w(this,window,this.connectOptions.timeout,this._disconnected,[h.CONNECT_TIMEOUT.code,f(h.CONNECT_TIMEOUT)])};d.prototype._schedule_message=function(a){this._msg_queue.push(a); |
39 | this.connected&&this._process_queue()};d.prototype.store=function(a,b){var c={type:b.type,messageIdentifier:b.messageIdentifier,version:1};switch(b.type){case 3:b.pubRecReceived&&(c.pubRecReceived=!0);c.payloadMessage={};for(var d="",e=b.payloadMessage.payloadBytes,g=0;g<e.length;g++)d=15>=e[g]?d+"0"+e[g].toString(16):d+e[g].toString(16);c.payloadMessage.payloadHex=d;c.payloadMessage.qos=b.payloadMessage.qos;c.payloadMessage.destinationName=b.payloadMessage.destinationName;b.payloadMessage.duplicate&& |
40 | (c.payloadMessage.duplicate=!0);b.payloadMessage.retained&&(c.payloadMessage.retained=!0);0===a.indexOf("Sent:")&&(void 0===b.sequence&&(b.sequence=++this._sequence),c.sequence=b.sequence);break;default:throw Error(f(h.INVALID_STORED_DATA,[key,c]));}localStorage.setItem(a+this._localKey+b.messageIdentifier,JSON.stringify(c))};d.prototype.restore=function(a){var b=localStorage.getItem(a),c=JSON.parse(b),d=new q(c.type,c);switch(c.type){case 3:for(var b=c.payloadMessage.payloadHex,e=new ArrayBuffer(b.length/ |
41 | 2),e=new Uint8Array(e),g=0;2<=b.length;){var m=parseInt(b.substring(0,2),16),b=b.substring(2,b.length);e[g++]=m}b=new Paho.MQTT.Message(e);b.qos=c.payloadMessage.qos;b.destinationName=c.payloadMessage.destinationName;c.payloadMessage.duplicate&&(b.duplicate=!0);c.payloadMessage.retained&&(b.retained=!0);d.payloadMessage=b;break;default:throw Error(f(h.INVALID_STORED_DATA,[a,b]));}0===a.indexOf("Sent:"+this._localKey)?(d.payloadMessage.duplicate=!0,this._sentMessages[d.messageIdentifier]=d):0===a.indexOf("Received:"+ |
42 | this._localKey)&&(this._receivedMessages[d.messageIdentifier]=d)};d.prototype._process_queue=function(){for(var a=null,b=this._msg_queue.reverse();a=b.pop();)this._socket_send(a),this._notify_msg_sent[a]&&(this._notify_msg_sent[a](),delete this._notify_msg_sent[a])};d.prototype._requires_ack=function(a){var b=Object.keys(this._sentMessages).length;if(b>this.maxMessageIdentifier)throw Error("Too many messages:"+b);for(;void 0!==this._sentMessages[this._message_identifier];)this._message_identifier++; |
43 | a.messageIdentifier=this._message_identifier;this._sentMessages[a.messageIdentifier]=a;3===a.type&&this.store("Sent:",a);this._message_identifier===this.maxMessageIdentifier&&(this._message_identifier=1)};d.prototype._on_socket_open=function(){var a=new q(1,this.connectOptions);a.clientId=this.clientId;this._socket_send(a)};d.prototype._on_socket_message=function(a){this._trace("Client._on_socket_message",a.data);a=this._deframeMessages(a.data);for(var b=0;b<a.length;b+=1)this._handleMessage(a[b])}; |
44 | d.prototype._deframeMessages=function(a){a=new Uint8Array(a);var b=[];if(this.receiveBuffer){var c=new Uint8Array(this.receiveBuffer.length+a.length);c.set(this.receiveBuffer);c.set(a,this.receiveBuffer.length);a=c;delete this.receiveBuffer}try{for(c=0;c<a.length;){var d;a:{var e=a,g=c,m=g,n=e[g],l=n>>4,t=n&15,g=g+1,x=void 0,C=0,p=1;do{if(g==e.length){d=[null,m];break a}x=e[g++];C+=(x&127)*p;p*=128}while(0!==(x&128));x=g+C;if(x>e.length)d=[null,m];else{var y=new q(l);switch(l){case 2:e[g++]&1&&(y.sessionPresent= |
45 | !0);y.returnCode=e[g++];break;case 3:var m=t>>1&3,s=256*e[g]+e[g+1],g=g+2,u=E(e,g,s),g=g+s;0<m&&(y.messageIdentifier=256*e[g]+e[g+1],g+=2);var r=new Paho.MQTT.Message(e.subarray(g,x));1==(t&1)&&(r.retained=!0);8==(t&8)&&(r.duplicate=!0);r.qos=m;r.destinationName=u;y.payloadMessage=r;break;case 4:case 5:case 6:case 7:case 11:y.messageIdentifier=256*e[g]+e[g+1];break;case 9:y.messageIdentifier=256*e[g]+e[g+1],g+=2,y.returnCode=e.subarray(g,x)}d=[y,x]}}var v=d[0],c=d[1];if(null!==v)b.push(v);else break}c< |
46 | a.length&&(this.receiveBuffer=a.subarray(c))}catch(w){d="undefined"==w.hasOwnProperty("stack")?w.stack.toString():"No Error Stack Available";this._disconnected(h.INTERNAL_ERROR.code,f(h.INTERNAL_ERROR,[w.message,d]));return}return b};d.prototype._handleMessage=function(a){this._trace("Client._handleMessage",a);try{switch(a.type){case 2:this._connectTimeout.cancel();this._reconnectTimeout&&this._reconnectTimeout.cancel();if(this.connectOptions.cleanSession){for(var b in this._sentMessages){var c=this._sentMessages[b]; |
47 | localStorage.removeItem("Sent:"+this._localKey+c.messageIdentifier)}this._sentMessages={};for(b in this._receivedMessages){var d=this._receivedMessages[b];localStorage.removeItem("Received:"+this._localKey+d.messageIdentifier)}this._receivedMessages={}}if(0===a.returnCode)this.connected=!0,this.connectOptions.uris&&(this.hostIndex=this.connectOptions.uris.length);else{this._disconnected(h.CONNACK_RETURNCODE.code,f(h.CONNACK_RETURNCODE,[a.returnCode,H[a.returnCode]]));break}a=[];for(var e in this._sentMessages)this._sentMessages.hasOwnProperty(e)&& |
48 | a.push(this._sentMessages[e]);if(0<this._buffered_msg_queue.length){e=null;for(var g=this._buffered_msg_queue.reverse();e=g.pop();)a.push(e),this.onMessageDelivered&&(this._notify_msg_sent[e]=this.onMessageDelivered(e.payloadMessage))}a=a.sort(function(a,b){return a.sequence-b.sequence});for(var g=0,m=a.length;g<m;g++)if(c=a[g],3==c.type&&c.pubRecReceived){var n=new q(6,{messageIdentifier:c.messageIdentifier});this._schedule_message(n)}else this._schedule_message(c);if(this.connectOptions.onSuccess)this.connectOptions.onSuccess({invocationContext:this.connectOptions.invocationContext}); |
49 | c=!1;this._reconnecting&&(c=!0,this._reconnectInterval=1,this._reconnecting=!1);this._connected(c,this._wsuri);this._process_queue();break;case 3:this._receivePublish(a);break;case 4:if(c=this._sentMessages[a.messageIdentifier])if(delete this._sentMessages[a.messageIdentifier],localStorage.removeItem("Sent:"+this._localKey+a.messageIdentifier),this.onMessageDelivered)this.onMessageDelivered(c.payloadMessage);break;case 5:if(c=this._sentMessages[a.messageIdentifier])c.pubRecReceived=!0,n=new q(6,{messageIdentifier:a.messageIdentifier}), |
50 | this.store("Sent:",c),this._schedule_message(n);break;case 6:d=this._receivedMessages[a.messageIdentifier];localStorage.removeItem("Received:"+this._localKey+a.messageIdentifier);d&&(this._receiveMessage(d),delete this._receivedMessages[a.messageIdentifier]);var l=new q(7,{messageIdentifier:a.messageIdentifier});this._schedule_message(l);break;case 7:c=this._sentMessages[a.messageIdentifier];delete this._sentMessages[a.messageIdentifier];localStorage.removeItem("Sent:"+this._localKey+a.messageIdentifier); |
51 | if(this.onMessageDelivered)this.onMessageDelivered(c.payloadMessage);break;case 9:if(c=this._sentMessages[a.messageIdentifier]){c.timeOut&&c.timeOut.cancel();if(128===a.returnCode[0]){if(c.onFailure)c.onFailure(a.returnCode)}else if(c.onSuccess)c.onSuccess(a.returnCode);delete this._sentMessages[a.messageIdentifier]}break;case 11:if(c=this._sentMessages[a.messageIdentifier])c.timeOut&&c.timeOut.cancel(),c.callback&&c.callback(),delete this._sentMessages[a.messageIdentifier];break;case 13:this.sendPinger.reset(); |
52 | break;case 14:this._disconnected(h.INVALID_MQTT_MESSAGE_TYPE.code,f(h.INVALID_MQTT_MESSAGE_TYPE,[a.type]));break;default:this._disconnected(h.INVALID_MQTT_MESSAGE_TYPE.code,f(h.INVALID_MQTT_MESSAGE_TYPE,[a.type]))}}catch(t){c="undefined"==t.hasOwnProperty("stack")?t.stack.toString():"No Error Stack Available",this._disconnected(h.INTERNAL_ERROR.code,f(h.INTERNAL_ERROR,[t.message,c]))}};d.prototype._on_socket_error=function(a){this._reconnecting||this._disconnected(h.SOCKET_ERROR.code,f(h.SOCKET_ERROR, |
53 | [a.data]))};d.prototype._on_socket_close=function(){this._reconnecting||this._disconnected(h.SOCKET_CLOSE.code,f(h.SOCKET_CLOSE))};d.prototype._socket_send=function(a){if(1==a.type){var b=this._traceMask(a,"password");this._trace("Client._socket_send",b)}else this._trace("Client._socket_send",a);this.socket.send(a.encode());this.sendPinger.reset()};d.prototype._receivePublish=function(a){switch(a.payloadMessage.qos){case "undefined":case 0:this._receiveMessage(a);break;case 1:var b=new q(4,{messageIdentifier:a.messageIdentifier}); |
54 | this._schedule_message(b);this._receiveMessage(a);break;case 2:this._receivedMessages[a.messageIdentifier]=a;this.store("Received:",a);a=new q(5,{messageIdentifier:a.messageIdentifier});this._schedule_message(a);break;default:throw Error("Invaild qos\x3d"+wireMmessage.payloadMessage.qos);}};d.prototype._receiveMessage=function(a){if(this.onMessageArrived)this.onMessageArrived(a.payloadMessage)};d.prototype._connected=function(a,b){if(this.onConnected)this.onConnected(a,b)};d.prototype._reconnect= |
55 | function(){this._trace("Client._reconnect");this.connected||(this._reconnecting=!0,this.sendPinger.cancel(),this.receivePinger.cancel(),128>this._reconnectInterval&&(this._reconnectInterval*=2),this.connectOptions.uris?(this.hostIndex=0,this._doConnect(this.connectOptions.uris[0])):this._doConnect(this.uri))};d.prototype._disconnected=function(a,b){this._trace("Client._disconnected",a,b);if(void 0!==a&&this._reconnecting)this._reconnectTimeout=new w(this,window,this._reconnectInterval,this._reconnect); |
56 | else if(this.sendPinger.cancel(),this.receivePinger.cancel(),this._connectTimeout&&(this._connectTimeout.cancel(),this._connectTimeout=null),this._msg_queue=[],this._buffered_msg_queue=[],this._notify_msg_sent={},this.socket&&(this.socket.onopen=null,this.socket.onmessage=null,this.socket.onerror=null,this.socket.onclose=null,1===this.socket.readyState&&this.socket.close(),delete this.socket),this.connectOptions.uris&&this.hostIndex<this.connectOptions.uris.length-1)this.hostIndex++,this._doConnect(this.connectOptions.uris[this.hostIndex]); |
57 | else if(void 0===a&&(a=h.OK.code,b=f(h.OK)),this.connected){this.connected=!1;if(this.onConnectionLost)this.onConnectionLost({errorCode:a,errorMessage:b,reconnect:this.connectOptions.reconnect,uri:this._wsuri});a!==h.OK.code&&this.connectOptions.reconnect&&(this._reconnectInterval=1,this._reconnect())}else if(4===this.connectOptions.mqttVersion&&!1===this.connectOptions.mqttVersionExplicit)this._trace("Failed to connect V4, dropping back to V3"),this.connectOptions.mqttVersion=3,this.connectOptions.uris? |
58 | (this.hostIndex=0,this._doConnect(this.connectOptions.uris[0])):this._doConnect(this.uri);else if(this.connectOptions.onFailure)this.connectOptions.onFailure({invocationContext:this.connectOptions.invocationContext,errorCode:a,errorMessage:b})};d.prototype._trace=function(){if(this.traceFunction){for(var a in arguments)"undefined"!==typeof arguments[a]&&arguments.splice(a,1,JSON.stringify(arguments[a]));a=Array.prototype.slice.call(arguments).join("");this.traceFunction({severity:"Debug",message:a})}if(null!== |
59 | this._traceBuffer){a=0;for(var b=arguments.length;a<b;a++)this._traceBuffer.length==this._MAX_TRACE_ENTRIES&&this._traceBuffer.shift(),0===a?this._traceBuffer.push(arguments[a]):"undefined"===typeof arguments[a]?this._traceBuffer.push(arguments[a]):this._traceBuffer.push(" "+JSON.stringify(arguments[a]))}};d.prototype._traceMask=function(a,b){var c={},d;for(d in a)a.hasOwnProperty(d)&&(c[d]=d==b?"******":a[d]);return c};var G=function(a,b,c,k){var e;if("string"!==typeof a)throw Error(f(h.INVALID_TYPE, |
60 | [typeof a,"host"]));if(2==arguments.length){k=b;e=a;var g=e.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/);if(g)a=g[4]||g[2],b=parseInt(g[7]),c=g[8];else throw Error(f(h.INVALID_ARGUMENT,[a,"host"]));}else{3==arguments.length&&(k=c,c="/mqtt");if("number"!==typeof b||0>b)throw Error(f(h.INVALID_TYPE,[typeof b,"port"]));if("string"!==typeof c)throw Error(f(h.INVALID_TYPE,[typeof c,"path"]));e="ws://"+(-1!==a.indexOf(":")&&"["!==a.slice(0,1)&&"]"!==a.slice(-1)?"["+a+"]":a)+":"+b+c}for(var m= |
61 | g=0;m<k.length;m++){var n=k.charCodeAt(m);55296<=n&&56319>=n&&m++;g++}if("string"!==typeof k||65535<g)throw Error(f(h.INVALID_ARGUMENT,[k,"clientId"]));var l=new d(e,a,b,c,k);this._getHost=function(){return a};this._setHost=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getPort=function(){return b};this._setPort=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getPath=function(){return c};this._setPath=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getURI=function(){return e}; |
62 | this._setURI=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getClientId=function(){return l.clientId};this._setClientId=function(){throw Error(f(h.UNSUPPORTED_OPERATION));};this._getOnConnected=function(){return l.onConnected};this._setOnConnected=function(a){if("function"===typeof a)l.onConnected=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onConnected"]));};this._getDisconnectedPublishing=function(){return l.disconnectedPublishing};this._setDisconnectedPublishing=function(a){l.disconnectedPublishing= |
63 | a};this._getDisconnectedBufferSize=function(){return l.disconnectedBufferSize};this._setDisconnectedBufferSize=function(a){l.disconnectedBufferSize=a};this._getOnConnectionLost=function(){return l.onConnectionLost};this._setOnConnectionLost=function(a){if("function"===typeof a)l.onConnectionLost=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onConnectionLost"]));};this._getOnMessageDelivered=function(){return l.onMessageDelivered};this._setOnMessageDelivered=function(a){if("function"===typeof a)l.onMessageDelivered= |
64 | a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onMessageDelivered"]));};this._getOnMessageArrived=function(){return l.onMessageArrived};this._setOnMessageArrived=function(a){if("function"===typeof a)l.onMessageArrived=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onMessageArrived"]));};this._getTrace=function(){return l.traceFunction};this._setTrace=function(a){if("function"===typeof a)l.traceFunction=a;else throw Error(f(h.INVALID_TYPE,[typeof a,"onTrace"]));};this.connect=function(a){a=a||{};z(a, |
65 | {timeout:"number",userName:"string",password:"string",willMessage:"object",keepAliveInterval:"number",cleanSession:"boolean",useSSL:"boolean",invocationContext:"object",onSuccess:"function",onFailure:"function",hosts:"object",ports:"object",reconnect:"boolean",mqttVersion:"number",mqttVersionExplicit:"boolean",uris:"object"});void 0===a.keepAliveInterval&&(a.keepAliveInterval=60);if(4<a.mqttVersion||3>a.mqttVersion)throw Error(f(h.INVALID_ARGUMENT,[a.mqttVersion,"connectOptions.mqttVersion"]));void 0=== |
66 | a.mqttVersion?(a.mqttVersionExplicit=!1,a.mqttVersion=4):a.mqttVersionExplicit=!0;if(void 0!==a.password&&void 0===a.userName)throw Error(f(h.INVALID_ARGUMENT,[a.password,"connectOptions.password"]));if(a.willMessage){if(!(a.willMessage instanceof r))throw Error(f(h.INVALID_TYPE,[a.willMessage,"connectOptions.willMessage"]));a.willMessage.stringPayload=null;if("undefined"===typeof a.willMessage.destinationName)throw Error(f(h.INVALID_TYPE,[typeof a.willMessage.destinationName,"connectOptions.willMessage.destinationName"])); |
67 | }"undefined"===typeof a.cleanSession&&(a.cleanSession=!0);if(a.hosts){if(!(a.hosts instanceof Array))throw Error(f(h.INVALID_ARGUMENT,[a.hosts,"connectOptions.hosts"]));if(1>a.hosts.length)throw Error(f(h.INVALID_ARGUMENT,[a.hosts,"connectOptions.hosts"]));for(var b=!1,d=0;d<a.hosts.length;d++){if("string"!==typeof a.hosts[d])throw Error(f(h.INVALID_TYPE,[typeof a.hosts[d],"connectOptions.hosts["+d+"]"]));if(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/.test(a.hosts[d]))if(0===d)b=!0;else{if(!b)throw Error(f(h.INVALID_ARGUMENT, |
68 | [a.hosts[d],"connectOptions.hosts["+d+"]"]));}else if(b)throw Error(f(h.INVALID_ARGUMENT,[a.hosts[d],"connectOptions.hosts["+d+"]"]));}if(b)a.uris=a.hosts;else{if(!a.ports)throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));if(!(a.ports instanceof Array))throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));if(a.hosts.length!==a.ports.length)throw Error(f(h.INVALID_ARGUMENT,[a.ports,"connectOptions.ports"]));a.uris=[];for(d=0;d<a.hosts.length;d++){if("number"!==typeof a.ports[d]|| |
69 | 0>a.ports[d])throw Error(f(h.INVALID_TYPE,[typeof a.ports[d],"connectOptions.ports["+d+"]"]));var b=a.hosts[d],g=a.ports[d];e="ws://"+(-1!==b.indexOf(":")?"["+b+"]":b)+":"+g+c;a.uris.push(e)}}}l.connect(a)};this.subscribe=function(a,b){if("string"!==typeof a)throw Error("Invalid argument:"+a);b=b||{};z(b,{qos:"number",invocationContext:"object",onSuccess:"function",onFailure:"function",timeout:"number"});if(b.timeout&&!b.onFailure)throw Error("subscribeOptions.timeout specified with no onFailure callback."); |
70 | if("undefined"!==typeof b.qos&&0!==b.qos&&1!==b.qos&&2!==b.qos)throw Error(f(h.INVALID_ARGUMENT,[b.qos,"subscribeOptions.qos"]));l.subscribe(a,b)};this.unsubscribe=function(a,b){if("string"!==typeof a)throw Error("Invalid argument:"+a);b=b||{};z(b,{invocationContext:"object",onSuccess:"function",onFailure:"function",timeout:"number"});if(b.timeout&&!b.onFailure)throw Error("unsubscribeOptions.timeout specified with no onFailure callback.");l.unsubscribe(a,b)};this.send=function(a,b,c,d){var e;if(0=== |
71 | arguments.length)throw Error("Invalid argument.length");if(1==arguments.length){if(!(a instanceof r)&&"string"!==typeof a)throw Error("Invalid argument:"+typeof a);e=a;if("undefined"===typeof e.destinationName)throw Error(f(h.INVALID_ARGUMENT,[e.destinationName,"Message.destinationName"]));}else e=new r(b),e.destinationName=a,3<=arguments.length&&(e.qos=c),4<=arguments.length&&(e.retained=d);l.send(e)};this.publish=function(a,b,c,d){console.log("Publising message to: ",a);var e;if(0===arguments.length)throw Error("Invalid argument.length"); |
72 | if(1==arguments.length){if(!(a instanceof r)&&"string"!==typeof a)throw Error("Invalid argument:"+typeof a);e=a;if("undefined"===typeof e.destinationName)throw Error(f(h.INVALID_ARGUMENT,[e.destinationName,"Message.destinationName"]));}else e=new r(b),e.destinationName=a,3<=arguments.length&&(e.qos=c),4<=arguments.length&&(e.retained=d);l.send(e)};this.disconnect=function(){l.disconnect()};this.getTraceLog=function(){return l.getTraceLog()};this.startTrace=function(){l.startTrace()};this.stopTrace= |
73 | function(){l.stopTrace()};this.isConnected=function(){return l.connected}};G.prototype={get host(){return this._getHost()},set host(a){this._setHost(a)},get port(){return this._getPort()},set port(a){this._setPort(a)},get path(){return this._getPath()},set path(a){this._setPath(a)},get clientId(){return this._getClientId()},set clientId(a){this._setClientId(a)},get onConnected(){return this._getOnConnected()},set onConnected(a){this._setOnConnected(a)},get disconnectedPublishing(){return this._getDisconnectedPublishing()}, |
74 | set disconnectedPublishing(a){this._setDisconnectedPublishing(a)},get disconnectedBufferSize(){return this._getDisconnectedBufferSize()},set disconnectedBufferSize(a){this._setDisconnectedBufferSize(a)},get onConnectionLost(){return this._getOnConnectionLost()},set onConnectionLost(a){this._setOnConnectionLost(a)},get onMessageDelivered(){return this._getOnMessageDelivered()},set onMessageDelivered(a){this._setOnMessageDelivered(a)},get onMessageArrived(){return this._getOnMessageArrived()},set onMessageArrived(a){this._setOnMessageArrived(a)}, |
75 | get trace(){return this._getTrace()},set trace(a){this._setTrace(a)}};var r=function(a){var b;if("string"===typeof a||a instanceof ArrayBuffer||a instanceof Int8Array||a instanceof Uint8Array||a instanceof Int16Array||a instanceof Uint16Array||a instanceof Int32Array||a instanceof Uint32Array||a instanceof Float32Array||a instanceof Float64Array)b=a;else throw f(h.INVALID_ARGUMENT,[a,"newPayload"]);this._getPayloadString=function(){return"string"===typeof b?b:E(b,0,b.length)};this._getPayloadBytes= |
76 | function(){if("string"===typeof b){var a=new ArrayBuffer(n(b)),a=new Uint8Array(a);D(b,a,0);return a}return b};var c;this._getDestinationName=function(){return c};this._setDestinationName=function(a){if("string"===typeof a)c=a;else throw Error(f(h.INVALID_ARGUMENT,[a,"newDestinationName"]));};var d=0;this._getQos=function(){return d};this._setQos=function(a){if(0===a||1===a||2===a)d=a;else throw Error("Invalid argument:"+a);};var e=!1;this._getRetained=function(){return e};this._setRetained=function(a){if("boolean"=== |
77 | typeof a)e=a;else throw Error(f(h.INVALID_ARGUMENT,[a,"newRetained"]));};var g=!1;this._getDuplicate=function(){return g};this._setDuplicate=function(a){g=a}};r.prototype={get payloadString(){return this._getPayloadString()},get payloadBytes(){return this._getPayloadBytes()},get destinationName(){return this._getDestinationName()},set destinationName(a){this._setDestinationName(a)},get topic(){return this._getDestinationName()},set topic(a){this._setDestinationName(a)},get qos(){return this._getQos()}, |
78 | set qos(a){this._setQos(a)},get retained(){return this._getRetained()},set retained(a){this._setRetained(a)},get duplicate(){return this._getDuplicate()},set duplicate(a){this._setDuplicate(a)}};return{Client:G,Message:r}}(window)}); |
1 | /******************************************************************************* |
2 | * Copyright (c) 2013 IBM Corp. |
3 | * |
4 | * All rights reserved. This program and the accompanying materials |
5 | * are made available under the terms of the Eclipse Public License v1.0 |
6 | * and Eclipse Distribution License v1.0 which accompany this distribution. |
7 | * |
8 | * The Eclipse Public License is available at |
9 | * http://www.eclipse.org/legal/epl-v10.html |
10 | * and the Eclipse Distribution License is available at |
11 | * http://www.eclipse.org/org/documents/edl-v10.php. |
12 | * |
13 | * Contributors: |
14 | * Andrew Banks - initial API and implementation and initial documentation |
15 | *******************************************************************************/ |
16 | |
17 | |
18 | // Only expose a single object name in the global namespace. |
19 | // Everything must go through this module. Global Paho.MQTT module |
20 | // only has a single public function, client, which returns |
21 | // a Paho.MQTT client object given connection details. |
22 | |
23 | /** |
24 | * Send and receive messages using web browsers. |
25 | * <p> |
26 | * This programming interface lets a JavaScript client application use the MQTT V3.1 or |
27 | * V3.1.1 protocol to connect to an MQTT-supporting messaging server. |
28 | * |
29 | * The function supported includes: |
30 | * <ol> |
31 | * <li>Connecting to and disconnecting from a server. The server is identified by its host name and port number. |
32 | * <li>Specifying options that relate to the communications link with the server, |
33 | * for example the frequency of keep-alive heartbeats, and whether SSL/TLS is required. |
34 | * <li>Subscribing to and receiving messages from MQTT Topics. |
35 | * <li>Publishing messages to MQTT Topics. |
36 | * </ol> |
37 | * <p> |
38 | * The API consists of two main objects: |
39 | * <dl> |
40 | * <dt><b>{@link Paho.MQTT.Client}</b></dt> |
41 | * <dd>This contains methods that provide the functionality of the API, |
42 | * including provision of callbacks that notify the application when a message |
43 | * arrives from or is delivered to the messaging server, |
44 | * or when the status of its connection to the messaging server changes.</dd> |
45 | * <dt><b>{@link Paho.MQTT.Message}</b></dt> |
46 | * <dd>This encapsulates the payload of the message along with various attributes |
47 | * associated with its delivery, in particular the destination to which it has |
48 | * been (or is about to be) sent.</dd> |
49 | * </dl> |
50 | * <p> |
51 | * The programming interface validates parameters passed to it, and will throw |
52 | * an Error containing an error message intended for developer use, if it detects |
53 | * an error with any parameter. |
54 | * <p> |
55 | * Example: |
56 | * |
57 | * <code><pre> |
58 | client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId"); |
59 | client.onConnectionLost = onConnectionLost; |
60 | client.onMessageArrived = onMessageArrived; |
61 | client.connect({onSuccess:onConnect}); |
62 | |
63 | function onConnect() { |
64 | // Once a connection has been made, make a subscription and send a message. |
65 | console.log("onConnect"); |
66 | client.subscribe("/World"); |
67 | message = new Paho.MQTT.Message("Hello"); |
68 | message.destinationName = "/World"; |
69 | client.send(message); |
70 | }; |
71 | function onConnectionLost(responseObject) { |
72 | if (responseObject.errorCode !== 0) |
73 | console.log("onConnectionLost:"+responseObject.errorMessage); |
74 | }; |
75 | function onMessageArrived(message) { |
76 | console.log("onMessageArrived:"+message.payloadString); |
77 | client.disconnect(); |
78 | }; |
79 | * </pre></code> |
80 | * @namespace Paho.MQTT |
81 | */ |
82 | |
83 | /* jshint shadow:true */ |
84 | (function ExportLibrary(root, factory) { |
85 | if(typeof exports === 'object' && typeof module === 'object'){ |
86 | module.exports = factory(); |
87 | } else if (typeof define === 'function' && define.amd){ |
88 | define(factory); |
89 | } else if (typeof exports === 'object'){ |
90 | exports = factory(); |
91 | } else { |
92 | if (typeof root.Paho === 'undefined'){ |
93 | root.Paho = {}; |
94 | } |
95 | root.Paho.MQTT = factory(); |
96 | } |
97 | })(this, function LibraryFactory(){ |
98 | |
99 | |
100 | var PahoMQTT = (function (global) { |
101 | |
102 | // Private variables below, these are only visible inside the function closure |
103 | // which is used to define the module. |
104 | |
105 | var version = "@VERSION@"; |
106 | var buildLevel = "@BUILDLEVEL@"; |
107 | |
108 | /** |
109 | * Unique message type identifiers, with associated |
110 | * associated integer values. |
111 | * @private |
112 | */ |
113 | var MESSAGE_TYPE = { |
114 | CONNECT: 1, |
115 | CONNACK: 2, |
116 | PUBLISH: 3, |
117 | PUBACK: 4, |
118 | PUBREC: 5, |
119 | PUBREL: 6, |
120 | PUBCOMP: 7, |
121 | SUBSCRIBE: 8, |
122 | SUBACK: 9, |
123 | UNSUBSCRIBE: 10, |
124 | UNSUBACK: 11, |
125 | PINGREQ: 12, |
126 | PINGRESP: 13, |
127 | DISCONNECT: 14 |
128 | }; |
129 | |
130 | // Collection of utility methods used to simplify module code |
131 | // and promote the DRY pattern. |
132 | |
133 | /** |
134 | * Validate an object's parameter names to ensure they |
135 | * match a list of expected variables name for this option |
136 | * type. Used to ensure option object passed into the API don't |
137 | * contain erroneous parameters. |
138 | * @param {Object} obj - User options object |
139 | * @param {Object} keys - valid keys and types that may exist in obj. |
140 | * @throws {Error} Invalid option parameter found. |
141 | * @private |
142 | */ |
143 | var validate = function(obj, keys) { |
144 | for (var key in obj) { |
145 | if (obj.hasOwnProperty(key)) { |
146 | if (keys.hasOwnProperty(key)) { |
147 | if (typeof obj[key] !== keys[key]) |
148 | throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key])); |
149 | } else { |
150 | var errorStr = "Unknown property, " + key + ". Valid properties are:"; |
151 | for (var validKey in keys) |
152 | if (keys.hasOwnProperty(validKey)) |
153 | errorStr = errorStr+" "+validKey; |
154 | throw new Error(errorStr); |
155 | } |
156 | } |
157 | } |
158 | }; |
159 | |
160 | /** |
161 | * Return a new function which runs the user function bound |
162 | * to a fixed scope. |
163 | * @param {function} User function |
164 | * @param {object} Function scope |
165 | * @return {function} User function bound to another scope |
166 | * @private |
167 | */ |
168 | var scope = function (f, scope) { |
169 | return function () { |
170 | return f.apply(scope, arguments); |
171 | }; |
172 | }; |
173 | |
174 | /** |
175 | * Unique message type identifiers, with associated |
176 | * associated integer values. |
177 | * @private |
178 | */ |
179 | var ERROR = { |
180 | OK: {code:0, text:"AMQJSC0000I OK."}, |
181 | CONNECT_TIMEOUT: {code:1, text:"AMQJSC0001E Connect timed out."}, |
182 | SUBSCRIBE_TIMEOUT: {code:2, text:"AMQJS0002E Subscribe timed out."}, |
183 | UNSUBSCRIBE_TIMEOUT: {code:3, text:"AMQJS0003E Unsubscribe timed out."}, |
184 | PING_TIMEOUT: {code:4, text:"AMQJS0004E Ping timed out."}, |
185 | INTERNAL_ERROR: {code:5, text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"}, |
186 | CONNACK_RETURNCODE: {code:6, text:"AMQJS0006E Bad Connack return code:{0} {1}."}, |
187 | SOCKET_ERROR: {code:7, text:"AMQJS0007E Socket error:{0}."}, |
188 | SOCKET_CLOSE: {code:8, text:"AMQJS0008I Socket closed."}, |
189 | MALFORMED_UTF: {code:9, text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."}, |
190 | UNSUPPORTED: {code:10, text:"AMQJS0010E {0} is not supported by this browser."}, |
191 | INVALID_STATE: {code:11, text:"AMQJS0011E Invalid state {0}."}, |
192 | INVALID_TYPE: {code:12, text:"AMQJS0012E Invalid type {0} for {1}."}, |
193 | INVALID_ARGUMENT: {code:13, text:"AMQJS0013E Invalid argument {0} for {1}."}, |
194 | UNSUPPORTED_OPERATION: {code:14, text:"AMQJS0014E Unsupported operation."}, |
195 | INVALID_STORED_DATA: {code:15, text:"AMQJS0015E Invalid data in local storage key={0} value={1}."}, |
196 | INVALID_MQTT_MESSAGE_TYPE: {code:16, text:"AMQJS0016E Invalid MQTT message type {0}."}, |
197 | MALFORMED_UNICODE: {code:17, text:"AMQJS0017E Malformed Unicode string:{0} {1}."}, |
198 | BUFFER_FULL: {code:18, text:"AMQJS0018E Message buffer is full, maximum buffer size: {0}."}, |
199 | }; |
200 | |
201 | /** CONNACK RC Meaning. */ |
202 | var CONNACK_RC = { |
203 | 0:"Connection Accepted", |
204 | 1:"Connection Refused: unacceptable protocol version", |
205 | 2:"Connection Refused: identifier rejected", |
206 | 3:"Connection Refused: server unavailable", |
207 | 4:"Connection Refused: bad user name or password", |
208 | 5:"Connection Refused: not authorized" |
209 | }; |
210 | |
211 | /** |
212 | * Format an error message text. |
213 | * @private |
214 | * @param {error} ERROR.KEY value above. |
215 | * @param {substitutions} [array] substituted into the text. |
216 | * @return the text with the substitutions made. |
217 | */ |
218 | var format = function(error, substitutions) { |
219 | var text = error.text; |
220 | if (substitutions) { |
221 | var field,start; |
222 | for (var i=0; i<substitutions.length; i++) { |
223 | field = "{"+i+"}"; |
224 | start = text.indexOf(field); |
225 | if(start > 0) { |
226 | var part1 = text.substring(0,start); |
227 | var part2 = text.substring(start+field.length); |
228 | text = part1+substitutions[i]+part2; |
229 | } |
230 | } |
231 | } |
232 | return text; |
233 | }; |
234 | |
235 | //MQTT protocol and version 6 M Q I s d p 3 |
236 | var MqttProtoIdentifierv3 = [0x00,0x06,0x4d,0x51,0x49,0x73,0x64,0x70,0x03]; |
237 | //MQTT proto/version for 311 4 M Q T T 4 |
238 | var MqttProtoIdentifierv4 = [0x00,0x04,0x4d,0x51,0x54,0x54,0x04]; |
239 | |
240 | /** |
241 | * Construct an MQTT wire protocol message. |
242 | * @param type MQTT packet type. |
243 | * @param options optional wire message attributes. |
244 | * |
245 | * Optional properties |
246 | * |
247 | * messageIdentifier: message ID in the range [0..65535] |
248 | * payloadMessage: Application Message - PUBLISH only |
249 | * connectStrings: array of 0 or more Strings to be put into the CONNECT payload |
250 | * topics: array of strings (SUBSCRIBE, UNSUBSCRIBE) |
251 | * requestQoS: array of QoS values [0..2] |
252 | * |
253 | * "Flag" properties |
254 | * cleanSession: true if present / false if absent (CONNECT) |
255 | * willMessage: true if present / false if absent (CONNECT) |
256 | * isRetained: true if present / false if absent (CONNECT) |
257 | * userName: true if present / false if absent (CONNECT) |
258 | * password: true if present / false if absent (CONNECT) |
259 | * keepAliveInterval: integer [0..65535] (CONNECT) |
260 | * |
261 | * @private |
262 | * @ignore |
263 | */ |
264 | var WireMessage = function (type, options) { |
265 | this.type = type; |
266 | for (var name in options) { |
267 | if (options.hasOwnProperty(name)) { |
268 | this[name] = options[name]; |
269 | } |
270 | } |
271 | }; |
272 | |
273 | WireMessage.prototype.encode = function() { |
274 | // Compute the first byte of the fixed header |
275 | var first = ((this.type & 0x0f) << 4); |
276 | |
277 | /* |
278 | * Now calculate the length of the variable header + payload by adding up the lengths |
279 | * of all the component parts |
280 | */ |
281 | |
282 | var remLength = 0; |
283 | var topicStrLength = []; |
284 | var destinationNameLength = 0; |
285 | var willMessagePayloadBytes; |
286 | |
287 | // if the message contains a messageIdentifier then we need two bytes for that |
288 | if (this.messageIdentifier !== undefined) |
289 | remLength += 2; |
290 | |
291 | switch(this.type) { |
292 | // If this a Connect then we need to include 12 bytes for its header |
293 | case MESSAGE_TYPE.CONNECT: |
294 | switch(this.mqttVersion) { |
295 | case 3: |
296 | remLength += MqttProtoIdentifierv3.length + 3; |
297 | break; |
298 | case 4: |
299 | remLength += MqttProtoIdentifierv4.length + 3; |
300 | break; |
301 | } |
302 | |
303 | remLength += UTF8Length(this.clientId) + 2; |
304 | if (this.willMessage !== undefined) { |
305 | remLength += UTF8Length(this.willMessage.destinationName) + 2; |
306 | // Will message is always a string, sent as UTF-8 characters with a preceding length. |
307 | willMessagePayloadBytes = this.willMessage.payloadBytes; |
308 | if (!(willMessagePayloadBytes instanceof Uint8Array)) |
309 | willMessagePayloadBytes = new Uint8Array(payloadBytes); |
310 | remLength += willMessagePayloadBytes.byteLength +2; |
311 | } |
312 | if (this.userName !== undefined) |
313 | remLength += UTF8Length(this.userName) + 2; |
314 | if (this.password !== undefined) |
315 | remLength += UTF8Length(this.password) + 2; |
316 | break; |
317 | |
318 | // Subscribe, Unsubscribe can both contain topic strings |
319 | case MESSAGE_TYPE.SUBSCRIBE: |
320 | first |= 0x02; // Qos = 1; |
321 | for ( var i = 0; i < this.topics.length; i++) { |
322 | topicStrLength[i] = UTF8Length(this.topics[i]); |
323 | remLength += topicStrLength[i] + 2; |
324 | } |
325 | remLength += this.requestedQos.length; // 1 byte for each topic's Qos |
326 | // QoS on Subscribe only |
327 | break; |
328 | |
329 | case MESSAGE_TYPE.UNSUBSCRIBE: |
330 | first |= 0x02; // Qos = 1; |
331 | for ( var i = 0; i < this.topics.length; i++) { |
332 | topicStrLength[i] = UTF8Length(this.topics[i]); |
333 | remLength += topicStrLength[i] + 2; |
334 | } |
335 | break; |
336 | |
337 | case MESSAGE_TYPE.PUBREL: |
338 | first |= 0x02; // Qos = 1; |
339 | break; |
340 | |
341 | case MESSAGE_TYPE.PUBLISH: |
342 | if (this.payloadMessage.duplicate) first |= 0x08; |
343 | first = first |= (this.payloadMessage.qos << 1); |
344 | if (this.payloadMessage.retained) first |= 0x01; |
345 | destinationNameLength = UTF8Length(this.payloadMessage.destinationName); |
346 | remLength += destinationNameLength + 2; |
347 | var payloadBytes = this.payloadMessage.payloadBytes; |
348 | remLength += payloadBytes.byteLength; |
349 | if (payloadBytes instanceof ArrayBuffer) |
350 | payloadBytes = new Uint8Array(payloadBytes); |
351 | else if (!(payloadBytes instanceof Uint8Array)) |
352 | payloadBytes = new Uint8Array(payloadBytes.buffer); |
353 | break; |
354 | |
355 | case MESSAGE_TYPE.DISCONNECT: |
356 | break; |
357 | |
358 | default: |
359 | break; |
360 | } |
361 | |
362 | // Now we can allocate a buffer for the message |
363 | |
364 | var mbi = encodeMBI(remLength); // Convert the length to MQTT MBI format |
365 | var pos = mbi.length + 1; // Offset of start of variable header |
366 | var buffer = new ArrayBuffer(remLength + pos); |
367 | var byteStream = new Uint8Array(buffer); // view it as a sequence of bytes |
368 | |
369 | //Write the fixed header into the buffer |
370 | byteStream[0] = first; |
371 | byteStream.set(mbi,1); |
372 | |
373 | // If this is a PUBLISH then the variable header starts with a topic |
374 | if (this.type == MESSAGE_TYPE.PUBLISH) |
375 | pos = writeString(this.payloadMessage.destinationName, destinationNameLength, byteStream, pos); |
376 | // If this is a CONNECT then the variable header contains the protocol name/version, flags and keepalive time |
377 | |
378 | else if (this.type == MESSAGE_TYPE.CONNECT) { |
379 | switch (this.mqttVersion) { |
380 | case 3: |
381 | byteStream.set(MqttProtoIdentifierv3, pos); |
382 | pos += MqttProtoIdentifierv3.length; |
383 | break; |
384 | case 4: |
385 | byteStream.set(MqttProtoIdentifierv4, pos); |
386 | pos += MqttProtoIdentifierv4.length; |
387 | break; |
388 | } |
389 | var connectFlags = 0; |
390 | if (this.cleanSession) |
391 | connectFlags = 0x02; |
392 | if (this.willMessage !== undefined ) { |
393 | connectFlags |= 0x04; |
394 | connectFlags |= (this.willMessage.qos<<3); |
395 | if (this.willMessage.retained) { |
396 | connectFlags |= 0x20; |
397 | } |
398 | } |
399 | if (this.userName !== undefined) |
400 | connectFlags |= 0x80; |
401 | if (this.password !== undefined) |
402 | connectFlags |= 0x40; |
403 | byteStream[pos++] = connectFlags; |
404 | pos = writeUint16 (this.keepAliveInterval, byteStream, pos); |
405 | } |
406 | |
407 | // Output the messageIdentifier - if there is one |
408 | if (this.messageIdentifier !== undefined) |
409 | pos = writeUint16 (this.messageIdentifier, byteStream, pos); |
410 | |
411 | switch(this.type) { |
412 | case MESSAGE_TYPE.CONNECT: |
413 | pos = writeString(this.clientId, UTF8Length(this.clientId), byteStream, pos); |
414 | if (this.willMessage !== undefined) { |
415 | pos = writeString(this.willMessage.destinationName, UTF8Length(this.willMessage.destinationName), byteStream, pos); |
416 | pos = writeUint16(willMessagePayloadBytes.byteLength, byteStream, pos); |
417 | byteStream.set(willMessagePayloadBytes, pos); |
418 | pos += willMessagePayloadBytes.byteLength; |
419 | |
420 | } |
421 | if (this.userName !== undefined) |
422 | pos = writeString(this.userName, UTF8Length(this.userName), byteStream, pos); |
423 | if (this.password !== undefined) |
424 | pos = writeString(this.password, UTF8Length(this.password), byteStream, pos); |
425 | break; |
426 | |
427 | case MESSAGE_TYPE.PUBLISH: |
428 | // PUBLISH has a text or binary payload, if text do not add a 2 byte length field, just the UTF characters. |
429 | byteStream.set(payloadBytes, pos); |
430 | |
431 | break; |
432 | |
433 | // case MESSAGE_TYPE.PUBREC: |
434 | // case MESSAGE_TYPE.PUBREL: |
435 | // case MESSAGE_TYPE.PUBCOMP: |
436 | // break; |
437 | |
438 | case MESSAGE_TYPE.SUBSCRIBE: |
439 | // SUBSCRIBE has a list of topic strings and request QoS |
440 | for (var i=0; i<this.topics.length; i++) { |
441 | pos = writeString(this.topics[i], topicStrLength[i], byteStream, pos); |
442 | byteStream[pos++] = this.requestedQos[i]; |
443 | } |
444 | break; |
445 | |
446 | case MESSAGE_TYPE.UNSUBSCRIBE: |
447 | // UNSUBSCRIBE has a list of topic strings |
448 | for (var i=0; i<this.topics.length; i++) |
449 | pos = writeString(this.topics[i], topicStrLength[i], byteStream, pos); |
450 | break; |
451 | |
452 | default: |
453 | // Do nothing. |
454 | } |
455 | |
456 | return buffer; |
457 | }; |
458 | |
459 | function decodeMessage(input,pos) { |
460 | var startingPos = pos; |
461 | var first = input[pos]; |
462 | var type = first >> 4; |
463 | var messageInfo = first &= 0x0f; |
464 | pos += 1; |
465 | |
466 | |
467 | // Decode the remaining length (MBI format) |
468 | |
469 | var digit; |
470 | var remLength = 0; |
471 | var multiplier = 1; |
472 | do { |
473 | if (pos == input.length) { |
474 | return [null,startingPos]; |
475 | } |
476 | digit = input[pos++]; |
477 | remLength += ((digit & 0x7F) * multiplier); |
478 | multiplier *= 128; |
479 | } while ((digit & 0x80) !== 0); |
480 | |
481 | var endPos = pos+remLength; |
482 | if (endPos > input.length) { |
483 | return [null,startingPos]; |
484 | } |
485 | |
486 | var wireMessage = new WireMessage(type); |
487 | switch(type) { |
488 | case MESSAGE_TYPE.CONNACK: |
489 | var connectAcknowledgeFlags = input[pos++]; |
490 | if (connectAcknowledgeFlags & 0x01) |
491 | wireMessage.sessionPresent = true; |
492 | wireMessage.returnCode = input[pos++]; |
493 | break; |
494 | |
495 | case MESSAGE_TYPE.PUBLISH: |
496 | var qos = (messageInfo >> 1) & 0x03; |
497 | |
498 | var len = readUint16(input, pos); |
499 | pos += 2; |
500 | var topicName = parseUTF8(input, pos, len); |
501 | pos += len; |
502 | // If QoS 1 or 2 there will be a messageIdentifier |
503 | if (qos > 0) { |
504 | wireMessage.messageIdentifier = readUint16(input, pos); |
505 | pos += 2; |
506 | } |
507 | |
508 | var message = new Paho.MQTT.Message(input.subarray(pos, endPos)); |
509 | if ((messageInfo & 0x01) == 0x01) |
510 | message.retained = true; |
511 | if ((messageInfo & 0x08) == 0x08) |
512 | message.duplicate = true; |
513 | message.qos = qos; |
514 | message.destinationName = topicName; |
515 | wireMessage.payloadMessage = message; |
516 | break; |
517 | |
518 | case MESSAGE_TYPE.PUBACK: |
519 | case MESSAGE_TYPE.PUBREC: |
520 | case MESSAGE_TYPE.PUBREL: |
521 | case MESSAGE_TYPE.PUBCOMP: |
522 | case MESSAGE_TYPE.UNSUBACK: |
523 | wireMessage.messageIdentifier = readUint16(input, pos); |
524 | break; |
525 | |
526 | case MESSAGE_TYPE.SUBACK: |
527 | wireMessage.messageIdentifier = readUint16(input, pos); |
528 | pos += 2; |
529 | wireMessage.returnCode = input.subarray(pos, endPos); |
530 | break; |
531 | |
532 | default: |
533 | break; |
534 | } |
535 | |
536 | return [wireMessage,endPos]; |
537 | } |
538 | |
539 | function writeUint16(input, buffer, offset) { |
540 | buffer[offset++] = input >> 8; //MSB |
541 | buffer[offset++] = input % 256; //LSB |
542 | return offset; |
543 | } |
544 | |
545 | function writeString(input, utf8Length, buffer, offset) { |
546 | offset = writeUint16(utf8Length, buffer, offset); |
547 | stringToUTF8(input, buffer, offset); |
548 | return offset + utf8Length; |
549 | } |
550 | |
551 | function readUint16(buffer, offset) { |
552 | return 256*buffer[offset] + buffer[offset+1]; |
553 | } |
554 | |
555 | /** |
556 | * Encodes an MQTT Multi-Byte Integer |
557 | * @private |
558 | */ |
559 | function encodeMBI(number) { |
560 | var output = new Array(1); |
561 | var numBytes = 0; |
562 | |
563 | do { |
564 | var digit = number % 128; |
565 | number = number >> 7; |
566 | if (number > 0) { |
567 | digit |= 0x80; |
568 | } |
569 | output[numBytes++] = digit; |
570 | } while ( (number > 0) && (numBytes<4) ); |
571 | |
572 | return output; |
573 | } |
574 | |
575 | /** |
576 | * Takes a String and calculates its length in bytes when encoded in UTF8. |
577 | * @private |
578 | */ |
579 | function UTF8Length(input) { |
580 | var output = 0; |
581 | for (var i = 0; i<input.length; i++) |
582 | { |
583 | var charCode = input.charCodeAt(i); |
584 | if (charCode > 0x7FF) |
585 | { |
586 | // Surrogate pair means its a 4 byte character |
587 | if (0xD800 <= charCode && charCode <= 0xDBFF) |
588 | { |
589 | i++; |
590 | output++; |
591 | } |
592 | output +=3; |
593 | } |
594 | else if (charCode > 0x7F) |
595 | output +=2; |
596 | else |
597 | output++; |
598 | } |
599 | return output; |
600 | } |
601 | |
602 | /** |
603 | * Takes a String and writes it into an array as UTF8 encoded bytes. |
604 | * @private |
605 | */ |
606 | function stringToUTF8(input, output, start) { |
607 | var pos = start; |
608 | for (var i = 0; i<input.length; i++) { |
609 | var charCode = input.charCodeAt(i); |
610 | |
611 | // Check for a surrogate pair. |
612 | if (0xD800 <= charCode && charCode <= 0xDBFF) { |
613 | var lowCharCode = input.charCodeAt(++i); |
614 | if (isNaN(lowCharCode)) { |
615 | throw new Error(format(ERROR.MALFORMED_UNICODE, [charCode, lowCharCode])); |
616 | } |
617 | charCode = ((charCode - 0xD800)<<10) + (lowCharCode - 0xDC00) + 0x10000; |
618 | |
619 | } |
620 | |
621 | if (charCode <= 0x7F) { |
622 | output[pos++] = charCode; |
623 | } else if (charCode <= 0x7FF) { |
624 | output[pos++] = charCode>>6 & 0x1F | 0xC0; |
625 | output[pos++] = charCode & 0x3F | 0x80; |
626 | } else if (charCode <= 0xFFFF) { |
627 | output[pos++] = charCode>>12 & 0x0F | 0xE0; |
628 | output[pos++] = charCode>>6 & 0x3F | 0x80; |
629 | output[pos++] = charCode & 0x3F | 0x80; |
630 | } else { |
631 | output[pos++] = charCode>>18 & 0x07 | 0xF0; |
632 | output[pos++] = charCode>>12 & 0x3F | 0x80; |
633 | output[pos++] = charCode>>6 & 0x3F | 0x80; |
634 | output[pos++] = charCode & 0x3F | 0x80; |
635 | } |
636 | } |
637 | return output; |
638 | } |
639 | |
640 | function parseUTF8(input, offset, length) { |
641 | var output = ""; |
642 | var utf16; |
643 | var pos = offset; |
644 | |
645 | while (pos < offset+length) |
646 | { |
647 | var byte1 = input[pos++]; |
648 | if (byte1 < 128) |
649 | utf16 = byte1; |
650 | else |
651 | { |
652 | var byte2 = input[pos++]-128; |
653 | if (byte2 < 0) |
654 | throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16),""])); |
655 | if (byte1 < 0xE0) // 2 byte character |
656 | utf16 = 64*(byte1-0xC0) + byte2; |
657 | else |
658 | { |
659 | var byte3 = input[pos++]-128; |
660 | if (byte3 < 0) |
661 | throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16)])); |
662 | if (byte1 < 0xF0) // 3 byte character |
663 | utf16 = 4096*(byte1-0xE0) + 64*byte2 + byte3; |
664 | else |
665 | { |
666 | var byte4 = input[pos++]-128; |
667 | if (byte4 < 0) |
668 | throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); |
669 | if (byte1 < 0xF8) // 4 byte character |
670 | utf16 = 262144*(byte1-0xF0) + 4096*byte2 + 64*byte3 + byte4; |
671 | else // longer encodings are not supported |
672 | throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); |
673 | } |
674 | } |
675 | } |
676 | |
677 | if (utf16 > 0xFFFF) // 4 byte character - express as a surrogate pair |
678 | { |
679 | utf16 -= 0x10000; |
680 | output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead character |
681 | utf16 = 0xDC00 + (utf16 & 0x3FF); // trail character |
682 | } |
683 | output += String.fromCharCode(utf16); |
684 | } |
685 | return output; |
686 | } |
687 | |
688 | /** |
689 | * Repeat keepalive requests, monitor responses. |
690 | * @ignore |
691 | */ |
692 | var Pinger = function(client, window, keepAliveInterval) { |
693 | this._client = client; |
694 | this._window = window; |
695 | this._keepAliveInterval = keepAliveInterval*1000; |
696 | this.isReset = false; |
697 | |
698 | var pingReq = new WireMessage(MESSAGE_TYPE.PINGREQ).encode(); |
699 | |
700 | var doTimeout = function (pinger) { |
701 | return function () { |
702 | return doPing.apply(pinger); |
703 | }; |
704 | }; |
705 | |
706 | /** @ignore */ |
707 | var doPing = function() { |
708 | if (!this.isReset) { |
709 | this._client._trace("Pinger.doPing", "Timed out"); |
710 | this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT)); |
711 | } else { |
712 | this.isReset = false; |
713 | this._client._trace("Pinger.doPing", "send PINGREQ"); |
714 | this._client.socket.send(pingReq); |
715 | this.timeout = this._window.setTimeout(doTimeout(this), this._keepAliveInterval); |
716 | } |
717 | }; |
718 | |
719 | this.reset = function() { |
720 | this.isReset = true; |
721 | this._window.clearTimeout(this.timeout); |
722 | if (this._keepAliveInterval > 0) |
723 | this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval); |
724 | }; |
725 | |
726 | this.cancel = function() { |
727 | this._window.clearTimeout(this.timeout); |
728 | }; |
729 | }; |
730 | |
731 | /** |
732 | * Monitor request completion. |
733 | * @ignore |
734 | */ |
735 | var Timeout = function(client, window, timeoutSeconds, action, args) { |
736 | this._window = window; |
737 | if (!timeoutSeconds) |
738 | timeoutSeconds = 30; |
739 | |
740 | var doTimeout = function (action, client, args) { |
741 | return function () { |
742 | return action.apply(client, args); |
743 | }; |
744 | }; |
745 | this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000); |
746 | |
747 | this.cancel = function() { |
748 | this._window.clearTimeout(this.timeout); |
749 | }; |
750 | }; |
751 | |
752 | /* |
753 | * Internal implementation of the Websockets MQTT V3.1 client. |
754 | * |
755 | * @name Paho.MQTT.ClientImpl @constructor |
756 | * @param {String} host the DNS nameof the webSocket host. |
757 | * @param {Number} port the port number for that host. |
758 | * @param {String} clientId the MQ client identifier. |
759 | */ |
760 | var ClientImpl = function (uri, host, port, path, clientId) { |
761 | // Check dependencies are satisfied in this browser. |
762 | if (!("WebSocket" in global && global.WebSocket !== null)) { |
763 | throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"])); |
764 | } |
765 | if (!("localStorage" in global && global.localStorage !== null)) { |
766 | throw new Error(format(ERROR.UNSUPPORTED, ["localStorage"])); |
767 | } |
768 | if (!("ArrayBuffer" in global && global.ArrayBuffer !== null)) { |
769 | throw new Error(format(ERROR.UNSUPPORTED, ["ArrayBuffer"])); |
770 | } |
771 | this._trace("Paho.MQTT.Client", uri, host, port, path, clientId); |
772 | |
773 | this.host = host; |
774 | this.port = port; |
775 | this.path = path; |
776 | this.uri = uri; |
777 | this.clientId = clientId; |
778 | this._wsuri = null; |
779 | |
780 | // Local storagekeys are qualified with the following string. |
781 | // The conditional inclusion of path in the key is for backward |
782 | // compatibility to when the path was not configurable and assumed to |
783 | // be /mqtt |
784 | this._localKey=host+":"+port+(path!="/mqtt"?":"+path:"")+":"+clientId+":"; |
785 | |
786 | // Create private instance-only message queue |
787 | // Internal queue of messages to be sent, in sending order. |
788 | this._msg_queue = []; |
789 | this._buffered_msg_queue = []; |
790 | |
791 | // Messages we have sent and are expecting a response for, indexed by their respective message ids. |
792 | this._sentMessages = {}; |
793 | |
794 | // Messages we have received and acknowleged and are expecting a confirm message for |
795 | // indexed by their respective message ids. |
796 | this._receivedMessages = {}; |
797 | |
798 | // Internal list of callbacks to be executed when messages |
799 | // have been successfully sent over web socket, e.g. disconnect |
800 | // when it doesn't have to wait for ACK, just message is dispatched. |
801 | this._notify_msg_sent = {}; |
802 | |
803 | // Unique identifier for SEND messages, incrementing |
804 | // counter as messages are sent. |
805 | this._message_identifier = 1; |
806 | |
807 | // Used to determine the transmission sequence of stored sent messages. |
808 | this._sequence = 0; |
809 | |
810 | |
811 | // Load the local state, if any, from the saved version, only restore state relevant to this client. |
812 | for (var key in localStorage) |
813 | if ( key.indexOf("Sent:"+this._localKey) === 0 || key.indexOf("Received:"+this._localKey) === 0) |
814 | this.restore(key); |
815 | }; |
816 | |
817 | // Messaging Client public instance members. |
818 | ClientImpl.prototype.host = null; |
819 | ClientImpl.prototype.port = null; |
820 | ClientImpl.prototype.path = null; |
821 | ClientImpl.prototype.uri = null; |
822 | ClientImpl.prototype.clientId = null; |
823 | |
824 | // Messaging Client private instance members. |
825 | ClientImpl.prototype.socket = null; |
826 | /* true once we have received an acknowledgement to a CONNECT packet. */ |
827 | ClientImpl.prototype.connected = false; |
828 | /* The largest message identifier allowed, may not be larger than 2**16 but |
829 | * if set smaller reduces the maximum number of outbound messages allowed. |
830 | */ |
831 | ClientImpl.prototype.maxMessageIdentifier = 65536; |
832 | ClientImpl.prototype.connectOptions = null; |
833 | ClientImpl.prototype.hostIndex = null; |
834 | ClientImpl.prototype.onConnected = null; |
835 | ClientImpl.prototype.onConnectionLost = null; |
836 | ClientImpl.prototype.onMessageDelivered = null; |
837 | ClientImpl.prototype.onMessageArrived = null; |
838 | ClientImpl.prototype.traceFunction = null; |
839 | ClientImpl.prototype._msg_queue = null; |
840 | ClientImpl.prototype._buffered_msg_queue = null; |
841 | ClientImpl.prototype._connectTimeout = null; |
842 | /* The sendPinger monitors how long we allow before we send data to prove to the server that we are alive. */ |
843 | ClientImpl.prototype.sendPinger = null; |
844 | /* The receivePinger monitors how long we allow before we require evidence that the server is alive. */ |
845 | ClientImpl.prototype.receivePinger = null; |
846 | ClientImpl.prototype._reconnectInterval = 1; // Reconnect Delay, starts at 1 second |
847 | ClientImpl.prototype._reconnecting = false; |
848 | ClientImpl.prototype._reconnectTimeout = null; |
849 | ClientImpl.prototype.disconnectedPublishing = false; |
850 | ClientImpl.prototype.disconnectedBufferSize = 5000; |
851 | |
852 | ClientImpl.prototype.receiveBuffer = null; |
853 | |
854 | ClientImpl.prototype._traceBuffer = null; |
855 | ClientImpl.prototype._MAX_TRACE_ENTRIES = 100; |
856 | |
857 | ClientImpl.prototype.connect = function (connectOptions) { |
858 | var connectOptionsMasked = this._traceMask(connectOptions, "password"); |
859 | this._trace("Client.connect", connectOptionsMasked, this.socket, this.connected); |
860 | |
861 | if (this.connected) |
862 | throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); |
863 | if (this.socket) |
864 | throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); |
865 | |
866 | if (this._reconnecting) { |
867 | // connect() function is called while reconnect is in progress. |
868 | // Terminate the auto reconnect process to use new connect options. |
869 | this._reconnectTimeout.cancel(); |
870 | this._reconnectTimeout = null; |
871 | this._reconnecting = false; |
872 | } |
873 | |
874 | this.connectOptions = connectOptions; |
875 | this._reconnectInterval = 1; |
876 | this._reconnecting = false; |
877 | if (connectOptions.uris) { |
878 | this.hostIndex = 0; |
879 | this._doConnect(connectOptions.uris[0]); |
880 | } else { |
881 | this._doConnect(this.uri); |
882 | } |
883 | |
884 | }; |
885 | |
886 | ClientImpl.prototype.subscribe = function (filter, subscribeOptions) { |
887 | this._trace("Client.subscribe", filter, subscribeOptions); |
888 | |
889 | if (!this.connected) |
890 | throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); |
891 | |
892 | var wireMessage = new WireMessage(MESSAGE_TYPE.SUBSCRIBE); |
893 | wireMessage.topics=[filter]; |
894 | if (subscribeOptions.qos !== undefined) |
895 | wireMessage.requestedQos = [subscribeOptions.qos]; |
896 | else |
897 | wireMessage.requestedQos = [0]; |
898 | |
899 | if (subscribeOptions.onSuccess) { |
900 | wireMessage.onSuccess = function(grantedQos) {subscribeOptions.onSuccess({invocationContext:subscribeOptions.invocationContext,grantedQos:grantedQos});}; |
901 | } |
902 | |
903 | if (subscribeOptions.onFailure) { |
904 | wireMessage.onFailure = function(errorCode) {subscribeOptions.onFailure({invocationContext:subscribeOptions.invocationContext,errorCode:errorCode, errorMessage:format(errorCode)});}; |
905 | } |
906 | |
907 | if (subscribeOptions.timeout) { |
908 | wireMessage.timeOut = new Timeout(this, window, subscribeOptions.timeout, subscribeOptions.onFailure, |
909 | [{invocationContext:subscribeOptions.invocationContext, |
910 | errorCode:ERROR.SUBSCRIBE_TIMEOUT.code, |
911 | errorMessage:format(ERROR.SUBSCRIBE_TIMEOUT)}]); |
912 | } |
913 | |
914 | // All subscriptions return a SUBACK. |
915 | this._requires_ack(wireMessage); |
916 | this._schedule_message(wireMessage); |
917 | }; |
918 | |
919 | /** @ignore */ |
920 | ClientImpl.prototype.unsubscribe = function(filter, unsubscribeOptions) { |
921 | this._trace("Client.unsubscribe", filter, unsubscribeOptions); |
922 | |
923 | if (!this.connected) |
924 | throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); |
925 | |
926 | var wireMessage = new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE); |
927 | wireMessage.topics = [filter]; |
928 | |
929 | if (unsubscribeOptions.onSuccess) { |
930 | wireMessage.callback = function() {unsubscribeOptions.onSuccess({invocationContext:unsubscribeOptions.invocationContext});}; |
931 | } |
932 | if (unsubscribeOptions.timeout) { |
933 | wireMessage.timeOut = new Timeout(this, window, unsubscribeOptions.timeout, unsubscribeOptions.onFailure, |
934 | [{invocationContext:unsubscribeOptions.invocationContext, |
935 | errorCode:ERROR.UNSUBSCRIBE_TIMEOUT.code, |
936 | errorMessage:format(ERROR.UNSUBSCRIBE_TIMEOUT)}]); |
937 | } |
938 | |
939 | // All unsubscribes return a SUBACK. |
940 | this._requires_ack(wireMessage); |
941 | this._schedule_message(wireMessage); |
942 | }; |
943 | |
944 | ClientImpl.prototype.send = function (message) { |
945 | this._trace("Client.send", message); |
946 | |
947 | wireMessage = new WireMessage(MESSAGE_TYPE.PUBLISH); |
948 | wireMessage.payloadMessage = message; |
949 | |
950 | if (this.connected) { |
951 | // Mark qos 1 & 2 message as "ACK required" |
952 | // For qos 0 message, invoke onMessageDelivered callback if there is one. |
953 | // Then schedule the message. |
954 | if (message.qos > 0) { |
955 | this._requires_ack(wireMessage); |
956 | } else if (this.onMessageDelivered) { |
957 | this._notify_msg_sent[wireMessage] = this.onMessageDelivered(wireMessage.payloadMessage); |
958 | } |
959 | this._schedule_message(wireMessage); |
960 | } else { |
961 | // Currently disconnected, will not schedule this message |
962 | // Check if reconnecting is in progress and disconnected publish is enabled. |
963 | if (this._reconnecting && this.disconnectedPublishing) { |
964 | // Check the limit which include the "required ACK" messages |
965 | var messageCount = Object.keys(this._sentMessages).length + this._buffered_msg_queue.length; |
966 | if (messageCount > this.disconnectedBufferSize) { |
967 | throw new Error(format(ERROR.BUFFER_FULL, [this.disconnectedBufferSize])); |
968 | } else { |
969 | if (message.qos > 0) { |
970 | // Mark this message as "ACK required" |
971 | this._requires_ack(wireMessage); |
972 | } else { |
973 | wireMessage.sequence = ++this._sequence; |
974 | this._buffered_msg_queue.push(wireMessage); |
975 | } |
976 | } |
977 | } else { |
978 | throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); |
979 | } |
980 | } |
981 | }; |
982 | |
983 | ClientImpl.prototype.disconnect = function () { |
984 | this._trace("Client.disconnect"); |
985 | |
986 | if (this._reconnecting) { |
987 | // disconnect() function is called while reconnect is in progress. |
988 | // Terminate the auto reconnect process. |
989 | this._reconnectTimeout.cancel(); |
990 | this._reconnectTimeout = null; |
991 | this._reconnecting = false; |
992 | } |
993 | |
994 | if (!this.socket) |
995 | throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"])); |
996 | |
997 | wireMessage = new WireMessage(MESSAGE_TYPE.DISCONNECT); |
998 | |
999 | // Run the disconnected call back as soon as the message has been sent, |
1000 | // in case of a failure later on in the disconnect processing. |
1001 | // as a consequence, the _disconected call back may be run several times. |
1002 | this._notify_msg_sent[wireMessage] = scope(this._disconnected, this); |
1003 | |
1004 | this._schedule_message(wireMessage); |
1005 | }; |
1006 | |
1007 | ClientImpl.prototype.getTraceLog = function () { |
1008 | if ( this._traceBuffer !== null ) { |
1009 | this._trace("Client.getTraceLog", new Date()); |
1010 | this._trace("Client.getTraceLog in flight messages", this._sentMessages.length); |
1011 | for (var key in this._sentMessages) |
1012 | this._trace("_sentMessages ",key, this._sentMessages[key]); |
1013 | for (var key in this._receivedMessages) |
1014 | this._trace("_receivedMessages ",key, this._receivedMessages[key]); |
1015 | |
1016 | return this._traceBuffer; |
1017 | } |
1018 | }; |
1019 | |
1020 | ClientImpl.prototype.startTrace = function () { |
1021 | if ( this._traceBuffer === null ) { |
1022 | this._traceBuffer = []; |
1023 | } |
1024 | this._trace("Client.startTrace", new Date(), version); |
1025 | }; |
1026 | |
1027 | ClientImpl.prototype.stopTrace = function () { |
1028 | delete this._traceBuffer; |
1029 | }; |
1030 | |
1031 | ClientImpl.prototype._doConnect = function (wsurl) { |
1032 | // When the socket is open, this client will send the CONNECT WireMessage using the saved parameters. |
1033 | if (this.connectOptions.useSSL) { |
1034 | var uriParts = wsurl.split(":"); |
1035 | uriParts[0] = "wss"; |
1036 | wsurl = uriParts.join(":"); |
1037 | } |
1038 | this._wsuri = wsurl; |
1039 | this.connected = false; |
1040 | |
1041 | |
1042 | |
1043 | if (this.connectOptions.mqttVersion < 4) { |
1044 | this.socket = new WebSocket(wsurl, ["mqttv3.1"]); |
1045 | } else { |
1046 | this.socket = new WebSocket(wsurl, ["mqtt"]); |
1047 | } |
1048 | this.socket.binaryType = 'arraybuffer'; |
1049 | this.socket.onopen = scope(this._on_socket_open, this); |
1050 | this.socket.onmessage = scope(this._on_socket_message, this); |
1051 | this.socket.onerror = scope(this._on_socket_error, this); |
1052 | this.socket.onclose = scope(this._on_socket_close, this); |
1053 | |
1054 | this.sendPinger = new Pinger(this, window, this.connectOptions.keepAliveInterval); |
1055 | this.receivePinger = new Pinger(this, window, this.connectOptions.keepAliveInterval); |
1056 | if (this._connectTimeout) { |
1057 | this._connectTimeout.cancel(); |
1058 | this._connectTimeout = null; |
1059 | } |
1060 | this._connectTimeout = new Timeout(this, window, this.connectOptions.timeout, this._disconnected, [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]); |
1061 | }; |
1062 | |
1063 | |
1064 | // Schedule a new message to be sent over the WebSockets |
1065 | // connection. CONNECT messages cause WebSocket connection |
1066 | // to be started. All other messages are queued internally |
1067 | // until this has happened. When WS connection starts, process |
1068 | // all outstanding messages. |
1069 | ClientImpl.prototype._schedule_message = function (message) { |
1070 | this._msg_queue.push(message); |
1071 | // Process outstanding messages in the queue if we have an open socket, and have received CONNACK. |
1072 | if (this.connected) { |
1073 | this._process_queue(); |
1074 | } |
1075 | }; |
1076 | |
1077 | ClientImpl.prototype.store = function(prefix, wireMessage) { |
1078 | var storedMessage = {type:wireMessage.type, messageIdentifier:wireMessage.messageIdentifier, version:1}; |
1079 | |
1080 | switch(wireMessage.type) { |
1081 | case MESSAGE_TYPE.PUBLISH: |
1082 | if(wireMessage.pubRecReceived) |
1083 | storedMessage.pubRecReceived = true; |
1084 | |
1085 | // Convert the payload to a hex string. |
1086 | storedMessage.payloadMessage = {}; |
1087 | var hex = ""; |
1088 | var messageBytes = wireMessage.payloadMessage.payloadBytes; |
1089 | for (var i=0; i<messageBytes.length; i++) { |
1090 | if (messageBytes[i] <= 0xF) |
1091 | hex = hex+"0"+messageBytes[i].toString(16); |
1092 | else |
1093 | hex = hex+messageBytes[i].toString(16); |
1094 | } |
1095 | storedMessage.payloadMessage.payloadHex = hex; |
1096 | |
1097 | storedMessage.payloadMessage.qos = wireMessage.payloadMessage.qos; |
1098 | storedMessage.payloadMessage.destinationName = wireMessage.payloadMessage.destinationName; |
1099 | if (wireMessage.payloadMessage.duplicate) |
1100 | storedMessage.payloadMessage.duplicate = true; |
1101 | if (wireMessage.payloadMessage.retained) |
1102 | storedMessage.payloadMessage.retained = true; |
1103 | |
1104 | // Add a sequence number to sent messages. |
1105 | if ( prefix.indexOf("Sent:") === 0 ) { |
1106 | if ( wireMessage.sequence === undefined ) |
1107 | wireMessage.sequence = ++this._sequence; |
1108 | storedMessage.sequence = wireMessage.sequence; |
1109 | } |
1110 | break; |
1111 | |
1112 | default: |
1113 | throw Error(format(ERROR.INVALID_STORED_DATA, [key, storedMessage])); |
1114 | } |
1115 | localStorage.setItem(prefix+this._localKey+wireMessage.messageIdentifier, JSON.stringify(storedMessage)); |
1116 | }; |
1117 | |
1118 | ClientImpl.prototype.restore = function(key) { |
1119 | var value = localStorage.getItem(key); |
1120 | var storedMessage = JSON.parse(value); |
1121 | |
1122 | var wireMessage = new WireMessage(storedMessage.type, storedMessage); |
1123 | |
1124 | switch(storedMessage.type) { |
1125 | case MESSAGE_TYPE.PUBLISH: |
1126 | // Replace the payload message with a Message object. |
1127 | var hex = storedMessage.payloadMessage.payloadHex; |
1128 | var buffer = new ArrayBuffer((hex.length)/2); |
1129 | var byteStream = new Uint8Array(buffer); |
1130 | var i = 0; |
1131 | while (hex.length >= 2) { |
1132 | var x = parseInt(hex.substring(0, 2), 16); |
1133 | hex = hex.substring(2, hex.length); |
1134 | byteStream[i++] = x; |
1135 | } |
1136 | var payloadMessage = new Paho.MQTT.Message(byteStream); |
1137 | |
1138 | payloadMessage.qos = storedMessage.payloadMessage.qos; |
1139 | payloadMessage.destinationName = storedMessage.payloadMessage.destinationName; |
1140 | if (storedMessage.payloadMessage.duplicate) |
1141 | payloadMessage.duplicate = true; |
1142 | if (storedMessage.payloadMessage.retained) |
1143 | payloadMessage.retained = true; |
1144 | wireMessage.payloadMessage = payloadMessage; |
1145 | |
1146 | break; |
1147 | |
1148 | default: |
1149 | throw Error(format(ERROR.INVALID_STORED_DATA, [key, value])); |
1150 | } |
1151 | |
1152 | if (key.indexOf("Sent:"+this._localKey) === 0) { |
1153 | wireMessage.payloadMessage.duplicate = true; |
1154 | this._sentMessages[wireMessage.messageIdentifier] = wireMessage; |
1155 | } else if (key.indexOf("Received:"+this._localKey) === 0) { |
1156 | this._receivedMessages[wireMessage.messageIdentifier] = wireMessage; |
1157 | } |
1158 | }; |
1159 | |
1160 | ClientImpl.prototype._process_queue = function () { |
1161 | var message = null; |
1162 | // Process messages in order they were added |
1163 | var fifo = this._msg_queue.reverse(); |
1164 | |
1165 | // Send all queued messages down socket connection |
1166 | while ((message = fifo.pop())) { |
1167 | this._socket_send(message); |
1168 | // Notify listeners that message was successfully sent |
1169 | if (this._notify_msg_sent[message]) { |
1170 | this._notify_msg_sent[message](); |
1171 | delete this._notify_msg_sent[message]; |
1172 | } |
1173 | } |
1174 | }; |
1175 | |
1176 | /** |
1177 | * Expect an ACK response for this message. Add message to the set of in progress |
1178 | * messages and set an unused identifier in this message. |
1179 | * @ignore |
1180 | */ |
1181 | ClientImpl.prototype._requires_ack = function (wireMessage) { |
1182 | var messageCount = Object.keys(this._sentMessages).length; |
1183 | if (messageCount > this.maxMessageIdentifier) |
1184 | throw Error ("Too many messages:"+messageCount); |
1185 | |
1186 | while(this._sentMessages[this._message_identifier] !== undefined) { |
1187 | this._message_identifier++; |
1188 | } |
1189 | wireMessage.messageIdentifier = this._message_identifier; |
1190 | this._sentMessages[wireMessage.messageIdentifier] = wireMessage; |
1191 | if (wireMessage.type === MESSAGE_TYPE.PUBLISH) { |
1192 | this.store("Sent:", wireMessage); |
1193 | } |
1194 | if (this._message_identifier === this.maxMessageIdentifier) { |
1195 | this._message_identifier = 1; |
1196 | } |
1197 | }; |
1198 | |
1199 | /** |
1200 | * Called when the underlying websocket has been opened. |
1201 | * @ignore |
1202 | */ |
1203 | ClientImpl.prototype._on_socket_open = function () { |
1204 | // Create the CONNECT message object. |
1205 | var wireMessage = new WireMessage(MESSAGE_TYPE.CONNECT, this.connectOptions); |
1206 | wireMessage.clientId = this.clientId; |
1207 | this._socket_send(wireMessage); |
1208 | }; |
1209 | |
1210 | /** |
1211 | * Called when the underlying websocket has received a complete packet. |
1212 | * @ignore |
1213 | */ |
1214 | ClientImpl.prototype._on_socket_message = function (event) { |
1215 | this._trace("Client._on_socket_message", event.data); |
1216 | var messages = this._deframeMessages(event.data); |
1217 | for (var i = 0; i < messages.length; i+=1) { |
1218 | this._handleMessage(messages[i]); |
1219 | } |
1220 | }; |
1221 | |
1222 | ClientImpl.prototype._deframeMessages = function(data) { |
1223 | var byteArray = new Uint8Array(data); |
1224 | var messages = []; |
1225 | if (this.receiveBuffer) { |
1226 | var newData = new Uint8Array(this.receiveBuffer.length+byteArray.length); |
1227 | newData.set(this.receiveBuffer); |
1228 | newData.set(byteArray,this.receiveBuffer.length); |
1229 | byteArray = newData; |
1230 | delete this.receiveBuffer; |
1231 | } |
1232 | try { |
1233 | var offset = 0; |
1234 | while(offset < byteArray.length) { |
1235 | var result = decodeMessage(byteArray,offset); |
1236 | var wireMessage = result[0]; |
1237 | offset = result[1]; |
1238 | if (wireMessage !== null) { |
1239 | messages.push(wireMessage); |
1240 | } else { |
1241 | break; |
1242 | } |
1243 | } |
1244 | if (offset < byteArray.length) { |
1245 | this.receiveBuffer = byteArray.subarray(offset); |
1246 | } |
1247 | } catch (error) { |
1248 | var errorStack = ((error.hasOwnProperty('stack') == 'undefined') ? error.stack.toString() : "No Error Stack Available"); |
1249 | this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,errorStack])); |
1250 | return; |
1251 | } |
1252 | return messages; |
1253 | }; |
1254 | |
1255 | ClientImpl.prototype._handleMessage = function(wireMessage) { |
1256 | |
1257 | this._trace("Client._handleMessage", wireMessage); |
1258 | |
1259 | try { |
1260 | switch(wireMessage.type) { |
1261 | case MESSAGE_TYPE.CONNACK: |
1262 | this._connectTimeout.cancel(); |
1263 | if (this._reconnectTimeout) |
1264 | this._reconnectTimeout.cancel(); |
1265 | |
1266 | // If we have started using clean session then clear up the local state. |
1267 | if (this.connectOptions.cleanSession) { |
1268 | for (var key in this._sentMessages) { |
1269 | var sentMessage = this._sentMessages[key]; |
1270 | localStorage.removeItem("Sent:"+this._localKey+sentMessage.messageIdentifier); |
1271 | } |
1272 | this._sentMessages = {}; |
1273 | |
1274 | for (var key in this._receivedMessages) { |
1275 | var receivedMessage = this._receivedMessages[key]; |
1276 | localStorage.removeItem("Received:"+this._localKey+receivedMessage.messageIdentifier); |
1277 | } |
1278 | this._receivedMessages = {}; |
1279 | } |
1280 | // Client connected and ready for business. |
1281 | if (wireMessage.returnCode === 0) { |
1282 | |
1283 | this.connected = true; |
1284 | // Jump to the end of the list of uris and stop looking for a good host. |
1285 | |
1286 | if (this.connectOptions.uris) |
1287 | this.hostIndex = this.connectOptions.uris.length; |
1288 | |
1289 | } else { |
1290 | this._disconnected(ERROR.CONNACK_RETURNCODE.code , format(ERROR.CONNACK_RETURNCODE, [wireMessage.returnCode, CONNACK_RC[wireMessage.returnCode]])); |
1291 | break; |
1292 | } |
1293 | |
1294 | // Resend messages. |
1295 | var sequencedMessages = []; |
1296 | for (var msgId in this._sentMessages) { |
1297 | if (this._sentMessages.hasOwnProperty(msgId)) |
1298 | sequencedMessages.push(this._sentMessages[msgId]); |
1299 | } |
1300 | |
1301 | // Also schedule qos 0 buffered messages if any |
1302 | if (this._buffered_msg_queue.length > 0) { |
1303 | var msg = null; |
1304 | var fifo = this._buffered_msg_queue.reverse(); |
1305 | while ((msg = fifo.pop())) { |
1306 | sequencedMessages.push(msg); |
1307 | if (this.onMessageDelivered) |
1308 | this._notify_msg_sent[msg] = this.onMessageDelivered(msg.payloadMessage); |
1309 | } |
1310 | } |
1311 | |
1312 | // Sort sentMessages into the original sent order. |
1313 | var sequencedMessages = sequencedMessages.sort(function(a,b) {return a.sequence - b.sequence;} ); |
1314 | for (var i=0, len=sequencedMessages.length; i<len; i++) { |
1315 | var sentMessage = sequencedMessages[i]; |
1316 | if (sentMessage.type == MESSAGE_TYPE.PUBLISH && sentMessage.pubRecReceived) { |
1317 | var pubRelMessage = new WireMessage(MESSAGE_TYPE.PUBREL, {messageIdentifier:sentMessage.messageIdentifier}); |
1318 | this._schedule_message(pubRelMessage); |
1319 | } else { |
1320 | this._schedule_message(sentMessage); |
1321 | } |
1322 | } |
1323 | |
1324 | // Execute the connectOptions.onSuccess callback if there is one. |
1325 | // Will also now return if this connection was the result of an automatic |
1326 | // reconnect and which URI was successfully connected to. |
1327 | if (this.connectOptions.onSuccess) { |
1328 | this.connectOptions.onSuccess({invocationContext:this.connectOptions.invocationContext}); |
1329 | } |
1330 | |
1331 | var reconnected = false; |
1332 | if (this._reconnecting) { |
1333 | reconnected = true; |
1334 | this._reconnectInterval = 1; |
1335 | this._reconnecting = false; |
1336 | } |
1337 | |
1338 | // Execute the onConnected callback if there is one. |
1339 | this._connected(reconnected, this._wsuri); |
1340 | |
1341 | // Process all queued messages now that the connection is established. |
1342 | this._process_queue(); |
1343 | break; |
1344 | |
1345 | case MESSAGE_TYPE.PUBLISH: |
1346 | this._receivePublish(wireMessage); |
1347 | break; |
1348 | |
1349 | case MESSAGE_TYPE.PUBACK: |
1350 | var sentMessage = this._sentMessages[wireMessage.messageIdentifier]; |
1351 | // If this is a re flow of a PUBACK after we have restarted receivedMessage will not exist. |
1352 | if (sentMessage) { |
1353 | delete this._sentMessages[wireMessage.messageIdentifier]; |
1354 | localStorage.removeItem("Sent:"+this._localKey+wireMessage.messageIdentifier); |
1355 | if (this.onMessageDelivered) |
1356 | this.onMessageDelivered(sentMessage.payloadMessage); |
1357 | } |
1358 | break; |
1359 | |
1360 | case MESSAGE_TYPE.PUBREC: |
1361 | var sentMessage = this._sentMessages[wireMessage.messageIdentifier]; |
1362 | // If this is a re flow of a PUBREC after we have restarted receivedMessage will not exist. |
1363 | if (sentMessage) { |
1364 | sentMessage.pubRecReceived = true; |
1365 | var pubRelMessage = new WireMessage(MESSAGE_TYPE.PUBREL, {messageIdentifier:wireMessage.messageIdentifier}); |
1366 | this.store("Sent:", sentMessage); |
1367 | this._schedule_message(pubRelMessage); |
1368 | } |
1369 | break; |
1370 | |
1371 | case MESSAGE_TYPE.PUBREL: |
1372 | var receivedMessage = this._receivedMessages[wireMessage.messageIdentifier]; |
1373 | localStorage.removeItem("Received:"+this._localKey+wireMessage.messageIdentifier); |
1374 | // If this is a re flow of a PUBREL after we have restarted receivedMessage will not exist. |
1375 | if (receivedMessage) { |
1376 | this._receiveMessage(receivedMessage); |
1377 | delete this._receivedMessages[wireMessage.messageIdentifier]; |
1378 | } |
1379 | // Always flow PubComp, we may have previously flowed PubComp but the server lost it and restarted. |
1380 | var pubCompMessage = new WireMessage(MESSAGE_TYPE.PUBCOMP, {messageIdentifier:wireMessage.messageIdentifier}); |
1381 | this._schedule_message(pubCompMessage); |
1382 | |
1383 | |
1384 | break; |
1385 | |
1386 | case MESSAGE_TYPE.PUBCOMP: |
1387 | var sentMessage = this._sentMessages[wireMessage.messageIdentifier]; |
1388 | delete this._sentMessages[wireMessage.messageIdentifier]; |
1389 | localStorage.removeItem("Sent:"+this._localKey+wireMessage.messageIdentifier); |
1390 | if (this.onMessageDelivered) |
1391 | this.onMessageDelivered(sentMessage.payloadMessage); |
1392 | break; |
1393 | |
1394 | case MESSAGE_TYPE.SUBACK: |
1395 | var sentMessage = this._sentMessages[wireMessage.messageIdentifier]; |
1396 | if (sentMessage) { |
1397 | if(sentMessage.timeOut) |
1398 | sentMessage.timeOut.cancel(); |
1399 | // This will need to be fixed when we add multiple topic support |
1400 | if (wireMessage.returnCode[0] === 0x80) { |
1401 | if (sentMessage.onFailure) { |
1402 | sentMessage.onFailure(wireMessage.returnCode); |
1403 | } |
1404 | } else if (sentMessage.onSuccess) { |
1405 | sentMessage.onSuccess(wireMessage.returnCode); |
1406 | } |
1407 | delete this._sentMessages[wireMessage.messageIdentifier]; |
1408 | } |
1409 | break; |
1410 | |
1411 | case MESSAGE_TYPE.UNSUBACK: |
1412 | var sentMessage = this._sentMessages[wireMessage.messageIdentifier]; |
1413 | if (sentMessage) { |
1414 | if (sentMessage.timeOut) |
1415 | sentMessage.timeOut.cancel(); |
1416 | if (sentMessage.callback) { |
1417 | sentMessage.callback(); |
1418 | } |
1419 | delete this._sentMessages[wireMessage.messageIdentifier]; |
1420 | } |
1421 | |
1422 | break; |
1423 | |
1424 | case MESSAGE_TYPE.PINGRESP: |
1425 | /* The sendPinger or receivePinger may have sent a ping, the receivePinger has already been reset. */ |
1426 | this.sendPinger.reset(); |
1427 | break; |
1428 | |
1429 | case MESSAGE_TYPE.DISCONNECT: |
1430 | // Clients do not expect to receive disconnect packets. |
1431 | this._disconnected(ERROR.INVALID_MQTT_MESSAGE_TYPE.code , format(ERROR.INVALID_MQTT_MESSAGE_TYPE, [wireMessage.type])); |
1432 | break; |
1433 | |
1434 | default: |
1435 | this._disconnected(ERROR.INVALID_MQTT_MESSAGE_TYPE.code , format(ERROR.INVALID_MQTT_MESSAGE_TYPE, [wireMessage.type])); |
1436 | } |
1437 | } catch (error) { |
1438 | var errorStack = ((error.hasOwnProperty('stack') == 'undefined') ? error.stack.toString() : "No Error Stack Available"); |
1439 | this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,errorStack])); |
1440 | return; |
1441 | } |
1442 | }; |
1443 | |
1444 | /** @ignore */ |
1445 | ClientImpl.prototype._on_socket_error = function (error) { |
1446 | if (!this._reconnecting) { |
1447 | this._disconnected(ERROR.SOCKET_ERROR.code , format(ERROR.SOCKET_ERROR, [error.data])); |
1448 | } |
1449 | }; |
1450 | |
1451 | /** @ignore */ |
1452 | ClientImpl.prototype._on_socket_close = function () { |
1453 | if (!this._reconnecting) { |
1454 | this._disconnected(ERROR.SOCKET_CLOSE.code , format(ERROR.SOCKET_CLOSE)); |
1455 | } |
1456 | }; |
1457 | |
1458 | /** @ignore */ |
1459 | ClientImpl.prototype._socket_send = function (wireMessage) { |
1460 | |
1461 | if (wireMessage.type == 1) { |
1462 | var wireMessageMasked = this._traceMask(wireMessage, "password"); |
1463 | this._trace("Client._socket_send", wireMessageMasked); |
1464 | } |
1465 | else this._trace("Client._socket_send", wireMessage); |
1466 | |
1467 | this.socket.send(wireMessage.encode()); |
1468 | /* We have proved to the server we are alive. */ |
1469 | this.sendPinger.reset(); |
1470 | }; |
1471 | |
1472 | /** @ignore */ |
1473 | ClientImpl.prototype._receivePublish = function (wireMessage) { |
1474 | switch(wireMessage.payloadMessage.qos) { |
1475 | case "undefined": |
1476 | case 0: |
1477 | this._receiveMessage(wireMessage); |
1478 | break; |
1479 | |
1480 | case 1: |
1481 | var pubAckMessage = new WireMessage(MESSAGE_TYPE.PUBACK, {messageIdentifier:wireMessage.messageIdentifier}); |
1482 | this._schedule_message(pubAckMessage); |
1483 | this._receiveMessage(wireMessage); |
1484 | break; |
1485 | |
1486 | case 2: |
1487 | this._receivedMessages[wireMessage.messageIdentifier] = wireMessage; |
1488 | this.store("Received:", wireMessage); |
1489 | var pubRecMessage = new WireMessage(MESSAGE_TYPE.PUBREC, {messageIdentifier:wireMessage.messageIdentifier}); |
1490 | this._schedule_message(pubRecMessage); |
1491 | |
1492 | break; |
1493 | |
1494 | default: |
1495 | throw Error("Invaild qos="+wireMmessage.payloadMessage.qos); |
1496 | } |
1497 | }; |
1498 | |
1499 | /** @ignore */ |
1500 | ClientImpl.prototype._receiveMessage = function (wireMessage) { |
1501 | if (this.onMessageArrived) { |
1502 | this.onMessageArrived(wireMessage.payloadMessage); |
1503 | } |
1504 | }; |
1505 | |
1506 | /** |
1507 | * Client has connected. |
1508 | * @param {reconnect} [boolean] indicate if this was a result of reconnect operation. |
1509 | * @param {uri} [string] fully qualified WebSocket URI of the server. |
1510 | */ |
1511 | ClientImpl.prototype._connected = function (reconnect, uri) { |
1512 | // Execute the onConnected callback if there is one. |
1513 | if (this.onConnected) |
1514 | this.onConnected(reconnect, uri); |
1515 | }; |
1516 | |
1517 | /** |
1518 | * Attempts to reconnect the client to the server. |
1519 | * For each reconnect attempt, will double the reconnect interval |
1520 | * up to 128 seconds. |
1521 | */ |
1522 | ClientImpl.prototype._reconnect = function () { |
1523 | this._trace("Client._reconnect"); |
1524 | if (!this.connected) { |
1525 | this._reconnecting = true; |
1526 | this.sendPinger.cancel(); |
1527 | this.receivePinger.cancel(); |
1528 | if (this._reconnectInterval < 128) |
1529 | this._reconnectInterval = this._reconnectInterval * 2; |
1530 | if (this.connectOptions.uris) { |
1531 | this.hostIndex = 0; |
1532 | this._doConnect(this.connectOptions.uris[0]); |
1533 | } else { |
1534 | this._doConnect(this.uri); |
1535 | } |
1536 | } |
1537 | }; |
1538 | |
1539 | /** |
1540 | * Client has disconnected either at its own request or because the server |
1541 | * or network disconnected it. Remove all non-durable state. |
1542 | * @param {errorCode} [number] the error number. |
1543 | * @param {errorText} [string] the error text. |
1544 | * @ignore |
1545 | */ |
1546 | ClientImpl.prototype._disconnected = function (errorCode, errorText) { |
1547 | this._trace("Client._disconnected", errorCode, errorText); |
1548 | |
1549 | if (errorCode !== undefined && this._reconnecting) { |
1550 | //Continue automatic reconnect process |
1551 | this._reconnectTimeout = new Timeout(this, window, this._reconnectInterval, this._reconnect); |
1552 | return; |
1553 | } |
1554 | |
1555 | this.sendPinger.cancel(); |
1556 | this.receivePinger.cancel(); |
1557 | if (this._connectTimeout) { |
1558 | this._connectTimeout.cancel(); |
1559 | this._connectTimeout = null; |
1560 | } |
1561 | |
1562 | // Clear message buffers. |
1563 | this._msg_queue = []; |
1564 | this._buffered_msg_queue = []; |
1565 | this._notify_msg_sent = {}; |
1566 | |
1567 | if (this.socket) { |
1568 | // Cancel all socket callbacks so that they cannot be driven again by this socket. |
1569 | this.socket.onopen = null; |
1570 | this.socket.onmessage = null; |
1571 | this.socket.onerror = null; |
1572 | this.socket.onclose = null; |
1573 | if (this.socket.readyState === 1) |
1574 | this.socket.close(); |
1575 | delete this.socket; |
1576 | } |
1577 | |
1578 | if (this.connectOptions.uris && this.hostIndex < this.connectOptions.uris.length-1) { |
1579 | // Try the next host. |
1580 | this.hostIndex++; |
1581 | this._doConnect(this.connectOptions.uris[this.hostIndex]); |
1582 | } else { |
1583 | |
1584 | if (errorCode === undefined) { |
1585 | errorCode = ERROR.OK.code; |
1586 | errorText = format(ERROR.OK); |
1587 | } |
1588 | |
1589 | // Run any application callbacks last as they may attempt to reconnect and hence create a new socket. |
1590 | if (this.connected) { |
1591 | this.connected = false; |
1592 | // Execute the connectionLostCallback if there is one, and we were connected. |
1593 | if (this.onConnectionLost) { |
1594 | this.onConnectionLost({errorCode:errorCode, errorMessage:errorText, reconnect:this.connectOptions.reconnect, uri:this._wsuri}); |
1595 | } |
1596 | if (errorCode !== ERROR.OK.code && this.connectOptions.reconnect) { |
1597 | // Start automatic reconnect process for the very first time since last successful connect. |
1598 | this._reconnectInterval = 1; |
1599 | this._reconnect(); |
1600 | return; |
1601 | } |
1602 | } else { |
1603 | // Otherwise we never had a connection, so indicate that the connect has failed. |
1604 | if (this.connectOptions.mqttVersion === 4 && this.connectOptions.mqttVersionExplicit === false) { |
1605 | this._trace("Failed to connect V4, dropping back to V3"); |
1606 | this.connectOptions.mqttVersion = 3; |
1607 | if (this.connectOptions.uris) { |
1608 | this.hostIndex = 0; |
1609 | this._doConnect(this.connectOptions.uris[0]); |
1610 | } else { |
1611 | this._doConnect(this.uri); |
1612 | } |
1613 | } else if(this.connectOptions.onFailure) { |
1614 | this.connectOptions.onFailure({invocationContext:this.connectOptions.invocationContext, errorCode:errorCode, errorMessage:errorText}); |
1615 | } |
1616 | } |
1617 | } |
1618 | }; |
1619 | |
1620 | /** @ignore */ |
1621 | ClientImpl.prototype._trace = function () { |
1622 | // Pass trace message back to client's callback function |
1623 | if (this.traceFunction) { |
1624 | for (var i in arguments) |
1625 | { |
1626 | if (typeof arguments[i] !== "undefined") |
1627 | arguments.splice(i, 1, JSON.stringify(arguments[i])); |
1628 | } |
1629 | var record = Array.prototype.slice.call(arguments).join(""); |
1630 | this.traceFunction ({severity: "Debug", message: record }); |
1631 | } |
1632 | |
1633 | //buffer style trace |
1634 | if ( this._traceBuffer !== null ) { |
1635 | for (var i = 0, max = arguments.length; i < max; i++) { |
1636 | if ( this._traceBuffer.length == this._MAX_TRACE_ENTRIES ) { |
1637 | this._traceBuffer.shift(); |
1638 | } |
1639 | if (i === 0) this._traceBuffer.push(arguments[i]); |
1640 | else if (typeof arguments[i] === "undefined" ) this._traceBuffer.push(arguments[i]); |
1641 | else this._traceBuffer.push(" "+JSON.stringify(arguments[i])); |
1642 | } |
1643 | } |
1644 | }; |
1645 | |
1646 | /** @ignore */ |
1647 | ClientImpl.prototype._traceMask = function (traceObject, masked) { |
1648 | var traceObjectMasked = {}; |
1649 | for (var attr in traceObject) { |
1650 | if (traceObject.hasOwnProperty(attr)) { |
1651 | if (attr == masked) |
1652 | traceObjectMasked[attr] = "******"; |
1653 | else |
1654 | traceObjectMasked[attr] = traceObject[attr]; |
1655 | } |
1656 | } |
1657 | return traceObjectMasked; |
1658 | }; |
1659 | |
1660 | // ------------------------------------------------------------------------ |
1661 | // Public Programming interface. |
1662 | // ------------------------------------------------------------------------ |
1663 | |
1664 | /** |
1665 | * The JavaScript application communicates to the server using a {@link Paho.MQTT.Client} object. |
1666 | * <p> |
1667 | * Most applications will create just one Client object and then call its connect() method, |
1668 | * however applications can create more than one Client object if they wish. |
1669 | * In this case the combination of host, port and clientId attributes must be different for each Client object. |
1670 | * <p> |
1671 | * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods |
1672 | * (even though the underlying protocol exchange might be synchronous in nature). |
1673 | * This means they signal their completion by calling back to the application, |
1674 | * via Success or Failure callback functions provided by the application on the method in question. |
1675 | * Such callbacks are called at most once per method invocation and do not persist beyond the lifetime |
1676 | * of the script that made the invocation. |
1677 | * <p> |
1678 | * In contrast there are some callback functions, most notably <i>onMessageArrived</i>, |
1679 | * that are defined on the {@link Paho.MQTT.Client} object. |
1680 | * These may get called multiple times, and aren't directly related to specific method invocations made by the client. |
1681 | * |
1682 | * @name Paho.MQTT.Client |
1683 | * |
1684 | * @constructor |
1685 | * |
1686 | * @param {string} host - the address of the messaging server, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address. |
1687 | * @param {number} port - the port number to connect to - only required if host is not a URI |
1688 | * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/mqtt'. |
1689 | * @param {string} clientId - the Messaging client identifier, between 1 and 23 characters in length. |
1690 | * |
1691 | * @property {string} host - <i>read only</i> the server's DNS hostname or dotted decimal IP address. |
1692 | * @property {number} port - <i>read only</i> the server's port. |
1693 | * @property {string} path - <i>read only</i> the server's path. |
1694 | * @property {string} clientId - <i>read only</i> used when connecting to the server. |
1695 | * @property {function} onConnectionLost - called when a connection has been lost. |
1696 | * after a connect() method has succeeded. |
1697 | * Establish the call back used when a connection has been lost. The connection may be |
1698 | * lost because the client initiates a disconnect or because the server or network |
1699 | * cause the client to be disconnected. The disconnect call back may be called without |
1700 | * the connectionComplete call back being invoked if, for example the client fails to |
1701 | * connect. |
1702 | * A single response object parameter is passed to the onConnectionLost callback containing the following fields: |
1703 | * <ol> |
1704 | * <li>errorCode |
1705 | * <li>errorMessage |
1706 | * </ol> |
1707 | * @property {function} onMessageDelivered - called when a message has been delivered. |
1708 | * All processing that this Client will ever do has been completed. So, for example, |
1709 | * in the case of a Qos=2 message sent by this client, the PubComp flow has been received from the server |
1710 | * and the message has been removed from persistent storage before this callback is invoked. |
1711 | * Parameters passed to the onMessageDelivered callback are: |
1712 | * <ol> |
1713 | * <li>{@link Paho.MQTT.Message} that was delivered. |
1714 | * </ol> |
1715 | * @property {function} onMessageArrived - called when a message has arrived in this Paho.MQTT.client. |
1716 | * Parameters passed to the onMessageArrived callback are: |
1717 | * <ol> |
1718 | * <li>{@link Paho.MQTT.Message} that has arrived. |
1719 | * </ol> |
1720 | * @property {function} onConnected - called when a connection is successfully made to the server. |
1721 | * after a connect() method. |
1722 | * Parameters passed to the onConnected callback are: |
1723 | * <ol> |
1724 | * <li>reconnect (boolean) - If true, the connection was the result of a reconnect.</li> |
1725 | * <li>URI (string) - The URI used to connect to the server.</li> |
1726 | * </ol> |
1727 | * @property {boolean} disconnectedPublishing - if set, will enable disconnected publishing in |
1728 | * in the event that the connection to the server is lost. |
1729 | * @property {number} disconnectedBufferSize - Used to set the maximum number of messages that the disconnected |
1730 | * buffer will hold before rejecting new messages. Default size: 5000 messages |
1731 | * @property {function} trace - called whenever trace is called. TODO |
1732 | */ |
1733 | var Client = function (host, port, path, clientId) { |
1734 | |
1735 | var uri; |
1736 | |
1737 | if (typeof host !== "string") |
1738 | throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"])); |
1739 | |
1740 | if (arguments.length == 2) { |
1741 | // host: must be full ws:// uri |
1742 | // port: clientId |
1743 | clientId = port; |
1744 | uri = host; |
1745 | var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/); |
1746 | if (match) { |
1747 | host = match[4]||match[2]; |
1748 | port = parseInt(match[7]); |
1749 | path = match[8]; |
1750 | } else { |
1751 | throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"])); |
1752 | } |
1753 | } else { |
1754 | if (arguments.length == 3) { |
1755 | clientId = path; |
1756 | path = "/mqtt"; |
1757 | } |
1758 | if (typeof port !== "number" || port < 0) |
1759 | throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"])); |
1760 | if (typeof path !== "string") |
1761 | throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"])); |
1762 | |
1763 | var ipv6AddSBracket = (host.indexOf(":") !== -1 && host.slice(0,1) !== "[" && host.slice(-1) !== "]"); |
1764 | uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path; |
1765 | } |
1766 | |
1767 | var clientIdLength = 0; |
1768 | for (var i = 0; i<clientId.length; i++) { |
1769 | var charCode = clientId.charCodeAt(i); |
1770 | if (0xD800 <= charCode && charCode <= 0xDBFF) { |
1771 | i++; // Surrogate pair. |
1772 | } |
1773 | clientIdLength++; |
1774 | } |
1775 | if (typeof clientId !== "string" || clientIdLength > 65535) |
1776 | throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"])); |
1777 | |
1778 | var client = new ClientImpl(uri, host, port, path, clientId); |
1779 | this._getHost = function() { return host; }; |
1780 | this._setHost = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; |
1781 | |
1782 | this._getPort = function() { return port; }; |
1783 | this._setPort = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; |
1784 | |
1785 | this._getPath = function() { return path; }; |
1786 | this._setPath = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; |
1787 | |
1788 | this._getURI = function() { return uri; }; |
1789 | this._setURI = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; |
1790 | |
1791 | this._getClientId = function() { return client.clientId; }; |
1792 | this._setClientId = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; |
1793 | |
1794 | this._getOnConnected = function() { return client.onConnected; }; |
1795 | this._setOnConnected = function(newOnConnected) { |
1796 | if (typeof newOnConnected === "function") |
1797 | client.onConnected = newOnConnected; |
1798 | else |
1799 | throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnected, "onConnected"])); |
1800 | }; |
1801 | |
1802 | this._getDisconnectedPublishing = function() { return client.disconnectedPublishing; }; |
1803 | this._setDisconnectedPublishing = function(newDisconnectedPublishing) { |
1804 | client.disconnectedPublishing = newDisconnectedPublishing; |
1805 | }; |
1806 | |
1807 | this._getDisconnectedBufferSize = function() { return client.disconnectedBufferSize; }; |
1808 | this._setDisconnectedBufferSize = function(newDisconnectedBufferSize) { |
1809 | client.disconnectedBufferSize = newDisconnectedBufferSize; |
1810 | }; |
1811 | |
1812 | this._getOnConnectionLost = function() { return client.onConnectionLost; }; |
1813 | this._setOnConnectionLost = function(newOnConnectionLost) { |
1814 | if (typeof newOnConnectionLost === "function") |
1815 | client.onConnectionLost = newOnConnectionLost; |
1816 | else |
1817 | throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"])); |
1818 | }; |
1819 | |
1820 | this._getOnMessageDelivered = function() { return client.onMessageDelivered; }; |
1821 | this._setOnMessageDelivered = function(newOnMessageDelivered) { |
1822 | if (typeof newOnMessageDelivered === "function") |
1823 | client.onMessageDelivered = newOnMessageDelivered; |
1824 | else |
1825 | throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageDelivered, "onMessageDelivered"])); |
1826 | }; |
1827 | |
1828 | this._getOnMessageArrived = function() { return client.onMessageArrived; }; |
1829 | this._setOnMessageArrived = function(newOnMessageArrived) { |
1830 | if (typeof newOnMessageArrived === "function") |
1831 | client.onMessageArrived = newOnMessageArrived; |
1832 | else |
1833 | throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"])); |
1834 | }; |
1835 | |
1836 | this._getTrace = function() { return client.traceFunction; }; |
1837 | this._setTrace = function(trace) { |
1838 | if(typeof trace === "function"){ |
1839 | client.traceFunction = trace; |
1840 | }else{ |
1841 | throw new Error(format(ERROR.INVALID_TYPE, [typeof trace, "onTrace"])); |
1842 | } |
1843 | }; |
1844 | |
1845 | /** |
1846 | * Connect this Messaging client to its server. |
1847 | * |
1848 | * @name Paho.MQTT.Client#connect |
1849 | * @function |
1850 | * @param {object} connectOptions - Attributes used with the connection. |
1851 | * @param {number} connectOptions.timeout - If the connect has not succeeded within this |
1852 | * number of seconds, it is deemed to have failed. |
1853 | * The default is 30 seconds. |
1854 | * @param {string} connectOptions.userName - Authentication username for this connection. |
1855 | * @param {string} connectOptions.password - Authentication password for this connection. |
1856 | * @param {Paho.MQTT.Message} connectOptions.willMessage - sent by the server when the client |
1857 | * disconnects abnormally. |
1858 | * @param {number} connectOptions.keepAliveInterval - the server disconnects this client if |
1859 | * there is no activity for this number of seconds. |
1860 | * The default value of 60 seconds is assumed if not set. |
1861 | * @param {boolean} connectOptions.cleanSession - if true(default) the client and server |
1862 | * persistent state is deleted on successful connect. |
1863 | * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection. |
1864 | * @param {object} connectOptions.invocationContext - passed to the onSuccess callback or onFailure callback. |
1865 | * @param {function} connectOptions.onSuccess - called when the connect acknowledgement |
1866 | * has been received from the server. |
1867 | * A single response object parameter is passed to the onSuccess callback containing the following fields: |
1868 | * <ol> |
1869 | * <li>invocationContext as passed in to the onSuccess method in the connectOptions. |
1870 | * </ol> |
1871 | * @param {function} connectOptions.onFailure - called when the connect request has failed or timed out. |
1872 | * A single response object parameter is passed to the onFailure callback containing the following fields: |
1873 | * <ol> |
1874 | * <li>invocationContext as passed in to the onFailure method in the connectOptions. |
1875 | * <li>errorCode a number indicating the nature of the error. |
1876 | * <li>errorMessage text describing the error. |
1877 | * </ol> |
1878 | * @param {array} connectOptions.hosts - If present this contains either a set of hostnames or fully qualified |
1879 | * WebSocket URIs (ws://iot.eclipse.org:80/ws), that are tried in order in place |
1880 | * of the host and port paramater on the construtor. The hosts are tried one at at time in order until |
1881 | * one of then succeeds. |
1882 | * @param {array} connectOptions.ports - If present the set of ports matching the hosts. If hosts contains URIs, this property |
1883 | * is not used. |
1884 | * @param {boolean} connectOptions.reconnect - Sets whether the client will automatically attempt to reconnect |
1885 | * to the server if the connection is lost. |
1886 | *<ul> |
1887 | *<li>If set to false, the client will not attempt to automatically reconnect to the server in the event that the |
1888 | * connection is lost.</li> |
1889 | *<li>If set to true, in the event that the connection is lost, the client will attempt to reconnect to the server. |
1890 | * It will initially wait 1 second before it attempts to reconnect, for every failed reconnect attempt, the delay |
1891 | * will double until it is at 2 minutes at which point the delay will stay at 2 minutes.</li> |
1892 | *</ul> |
1893 | * @param {number} connectOptions.mqttVersion - The version of MQTT to use to connect to the MQTT Broker. |
1894 | *<ul> |
1895 | *<li>3 - MQTT V3.1</li> |
1896 | *<li>4 - MQTT V3.1.1</li> |
1897 | *</ul> |
1898 | * @param {boolean} connectOptions.mqttVersionExplicit - If set to true, will force the connection to use the |
1899 | * selected MQTT Version or will fail to connect. |
1900 | * @param {array} connectOptions.uris - If present, should contain a list of fully qualified WebSocket uris |
1901 | * (e.g. ws://iot.eclipse.org:80/ws), that are tried in order in place of the host and port parameter of the construtor. |
1902 | * The uris are tried one at a time in order until one of them succeeds. Do not use this in conjunction with hosts as |
1903 | * the hosts array will be converted to uris and will overwrite this property. |
1904 | * @throws {InvalidState} If the client is not in disconnected state. The client must have received connectionLost |
1905 | * or disconnected before calling connect for a second or subsequent time. |
1906 | */ |
1907 | this.connect = function (connectOptions) { |
1908 | connectOptions = connectOptions || {} ; |
1909 | validate(connectOptions, {timeout:"number", |
1910 | userName:"string", |
1911 | password:"string", |
1912 | willMessage:"object", |
1913 | keepAliveInterval:"number", |
1914 | cleanSession:"boolean", |
1915 | useSSL:"boolean", |
1916 | invocationContext:"object", |
1917 | onSuccess:"function", |
1918 | onFailure:"function", |
1919 | hosts:"object", |
1920 | ports:"object", |
1921 | reconnect:"boolean", |
1922 | mqttVersion:"number", |
1923 | mqttVersionExplicit:"boolean", |
1924 | uris: "object"}); |
1925 | |
1926 | // If no keep alive interval is set, assume 60 seconds. |
1927 | if (connectOptions.keepAliveInterval === undefined) |
1928 | connectOptions.keepAliveInterval = 60; |
1929 | |
1930 | if (connectOptions.mqttVersion > 4 || connectOptions.mqttVersion < 3) { |
1931 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.mqttVersion, "connectOptions.mqttVersion"])); |
1932 | } |
1933 | |
1934 | if (connectOptions.mqttVersion === undefined) { |
1935 | connectOptions.mqttVersionExplicit = false; |
1936 | connectOptions.mqttVersion = 4; |
1937 | } else { |
1938 | connectOptions.mqttVersionExplicit = true; |
1939 | } |
1940 | |
1941 | //Check that if password is set, so is username |
1942 | if (connectOptions.password !== undefined && connectOptions.userName === undefined) |
1943 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.password, "connectOptions.password"])); |
1944 | |
1945 | if (connectOptions.willMessage) { |
1946 | if (!(connectOptions.willMessage instanceof Message)) |
1947 | throw new Error(format(ERROR.INVALID_TYPE, [connectOptions.willMessage, "connectOptions.willMessage"])); |
1948 | // The will message must have a payload that can be represented as a string. |
1949 | // Cause the willMessage to throw an exception if this is not the case. |
1950 | connectOptions.willMessage.stringPayload = null; |
1951 | |
1952 | if (typeof connectOptions.willMessage.destinationName === "undefined") |
1953 | throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.willMessage.destinationName, "connectOptions.willMessage.destinationName"])); |
1954 | } |
1955 | if (typeof connectOptions.cleanSession === "undefined") |
1956 | connectOptions.cleanSession = true; |
1957 | if (connectOptions.hosts) { |
1958 | |
1959 | if (!(connectOptions.hosts instanceof Array) ) |
1960 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); |
1961 | if (connectOptions.hosts.length <1 ) |
1962 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); |
1963 | |
1964 | var usingURIs = false; |
1965 | for (var i = 0; i<connectOptions.hosts.length; i++) { |
1966 | if (typeof connectOptions.hosts[i] !== "string") |
1967 | throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.hosts[i], "connectOptions.hosts["+i+"]"])); |
1968 | if (/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/.test(connectOptions.hosts[i])) { |
1969 | if (i === 0) { |
1970 | usingURIs = true; |
1971 | } else if (!usingURIs) { |
1972 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts[i], "connectOptions.hosts["+i+"]"])); |
1973 | } |
1974 | } else if (usingURIs) { |
1975 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts[i], "connectOptions.hosts["+i+"]"])); |
1976 | } |
1977 | } |
1978 | |
1979 | if (!usingURIs) { |
1980 | if (!connectOptions.ports) |
1981 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"])); |
1982 | if (!(connectOptions.ports instanceof Array) ) |
1983 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"])); |
1984 | if (connectOptions.hosts.length !== connectOptions.ports.length) |
1985 | throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.ports, "connectOptions.ports"])); |
1986 | |
1987 | connectOptions.uris = []; |
1988 | |
1989 | for (var i = 0; i<connectOptions.hosts.length; i++) { |
1990 | if (typeof connectOptions.ports[i] !== "number" || connectOptions.ports[i] < 0) |
1991 | throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.ports[i], "connectOptions.ports["+i+"]"])); |
1992 | var host = connectOptions.hosts[i]; |
1993 | var port = connectOptions.ports[i]; |
1994 | |
1995 | var ipv6 = (host.indexOf(":") !== -1); |
1996 | uri = "ws://"+(ipv6?"["+host+"]":host)+":"+port+path; |
1997 | connectOptions.uris.push(uri); |
1998 | } |
1999 | } else { |
2000 | connectOptions.uris = connectOptions.hosts; |
2001 | } |
2002 | } |
2003 | |
2004 | client.connect(connectOptions); |
2005 | }; |
2006 | |
2007 | /** |
2008 | * Subscribe for messages, request receipt of a copy of messages sent to the destinations described by the filter. |
2009 | * |
2010 | * @name Paho.MQTT.Client#subscribe |
2011 | * @function |
2012 | * @param {string} filter describing the destinations to receive messages from. |
2013 | * <br> |
2014 | * @param {object} subscribeOptions - used to control the subscription |
2015 | * |
2016 | * @param {number} subscribeOptions.qos - the maiximum qos of any publications sent |
2017 | * as a result of making this subscription. |
2018 | * @param {object} subscribeOptions.invocationContext - passed to the onSuccess callback |
2019 | * or onFailure callback. |
2020 | * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement |
2021 | * has been received from the server. |
2022 | * A single response object parameter is passed to the onSuccess callback containing the following fields: |
2023 | * <ol> |
2024 | * <li>invocationContext if set in the subscribeOptions. |
2025 | * </ol> |
2026 | * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out. |
2027 | * A single response object parameter is passed to the onFailure callback containing the following fields: |
2028 | * <ol> |
2029 | * <li>invocationContext - if set in the subscribeOptions. |
2030 | * <li>errorCode - a number indicating the nature of the error. |
2031 | * <li>errorMessage - text describing the error. |
2032 | * </ol> |
2033 | * @param {number} subscribeOptions.timeout - which, if present, determines the number of |
2034 | * seconds after which the onFailure calback is called. |
2035 | * The presence of a timeout does not prevent the onSuccess |
2036 | * callback from being called when the subscribe completes. |
2037 | * @throws {InvalidState} if the client is not in connected state. |
2038 | */ |
2039 | this.subscribe = function (filter, subscribeOptions) { |
2040 | if (typeof filter !== "string") |
2041 | throw new Error("Invalid argument:"+filter); |
2042 | subscribeOptions = subscribeOptions || {} ; |
2043 | validate(subscribeOptions, {qos:"number", |
2044 | invocationContext:"object", |
2045 | onSuccess:"function", |
2046 | onFailure:"function", |
2047 | timeout:"number" |
2048 | }); |
2049 | if (subscribeOptions.timeout && !subscribeOptions.onFailure) |
2050 | throw new Error("subscribeOptions.timeout specified with no onFailure callback."); |
2051 | if (typeof subscribeOptions.qos !== "undefined" && !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 )) |
2052 | throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"])); |
2053 | client.subscribe(filter, subscribeOptions); |
2054 | }; |
2055 | |
2056 | /** |
2057 | * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter. |
2058 | * |
2059 | * @name Paho.MQTT.Client#unsubscribe |
2060 | * @function |
2061 | * @param {string} filter - describing the destinations to receive messages from. |
2062 | * @param {object} unsubscribeOptions - used to control the subscription |
2063 | * @param {object} unsubscribeOptions.invocationContext - passed to the onSuccess callback |
2064 | or onFailure callback. |
2065 | * @param {function} unsubscribeOptions.onSuccess - called when the unsubscribe acknowledgement has been received from the server. |
2066 | * A single response object parameter is passed to the |
2067 | * onSuccess callback containing the following fields: |
2068 | * <ol> |
2069 | * <li>invocationContext - if set in the unsubscribeOptions. |
2070 | * </ol> |
2071 | * @param {function} unsubscribeOptions.onFailure called when the unsubscribe request has failed or timed out. |
2072 | * A single response object parameter is passed to the onFailure callback containing the following fields: |
2073 | * <ol> |
2074 | * <li>invocationContext - if set in the unsubscribeOptions. |
2075 | * <li>errorCode - a number indicating the nature of the error. |
2076 | * <li>errorMessage - text describing the error. |
2077 | * </ol> |
2078 | * @param {number} unsubscribeOptions.timeout - which, if present, determines the number of seconds |
2079 | * after which the onFailure callback is called. The presence of |
2080 | * a timeout does not prevent the onSuccess callback from being |
2081 | * called when the unsubscribe completes |
2082 | * @throws {InvalidState} if the client is not in connected state. |
2083 | */ |
2084 | this.unsubscribe = function (filter, unsubscribeOptions) { |
2085 | if (typeof filter !== "string") |
2086 | throw new Error("Invalid argument:"+filter); |
2087 | unsubscribeOptions = unsubscribeOptions || {} ; |
2088 | validate(unsubscribeOptions, {invocationContext:"object", |
2089 | onSuccess:"function", |
2090 | onFailure:"function", |
2091 | timeout:"number" |
2092 | }); |
2093 | if (unsubscribeOptions.timeout && !unsubscribeOptions.onFailure) |
2094 | throw new Error("unsubscribeOptions.timeout specified with no onFailure callback."); |
2095 | client.unsubscribe(filter, unsubscribeOptions); |
2096 | }; |
2097 | |
2098 | /** |
2099 | * Send a message to the consumers of the destination in the Message. |
2100 | * |
2101 | * @name Paho.MQTT.Client#send |
2102 | * @function |
2103 | * @param {string|Paho.MQTT.Message} topic - <b>mandatory</b> The name of the destination to which the message is to be sent. |
2104 | * - If it is the only parameter, used as Paho.MQTT.Message object. |
2105 | * @param {String|ArrayBuffer} payload - The message data to be sent. |
2106 | * @param {number} qos The Quality of Service used to deliver the message. |
2107 | * <dl> |
2108 | * <dt>0 Best effort (default). |
2109 | * <dt>1 At least once. |
2110 | * <dt>2 Exactly once. |
2111 | * </dl> |
2112 | * @param {Boolean} retained If true, the message is to be retained by the server and delivered |
2113 | * to both current and future subscriptions. |
2114 | * If false the server only delivers the message to current subscribers, this is the default for new Messages. |
2115 | * A received message has the retained boolean set to true if the message was published |
2116 | * with the retained boolean set to true |
2117 | * and the subscrption was made after the message has been published. |
2118 | * @throws {InvalidState} if the client is not connected. |
2119 | */ |
2120 | this.send = function (topic,payload,qos,retained) { |
2121 | var message ; |
2122 | |
2123 | if(arguments.length === 0){ |
2124 | throw new Error("Invalid argument."+"length"); |
2125 | |
2126 | }else if(arguments.length == 1) { |
2127 | |
2128 | if (!(topic instanceof Message) && (typeof topic !== "string")) |
2129 | throw new Error("Invalid argument:"+ typeof topic); |
2130 | |
2131 | message = topic; |
2132 | if (typeof message.destinationName === "undefined") |
2133 | throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"])); |
2134 | client.send(message); |
2135 | |
2136 | }else { |
2137 | //parameter checking in Message object |
2138 | message = new Message(payload); |
2139 | message.destinationName = topic; |
2140 | if(arguments.length >= 3) |
2141 | message.qos = qos; |
2142 | if(arguments.length >= 4) |
2143 | message.retained = retained; |
2144 | client.send(message); |
2145 | } |
2146 | }; |
2147 | |
2148 | /** |
2149 | * Publish a message to the consumers of the destination in the Message. |
2150 | * Synonym for Paho.Mqtt.Client#send |
2151 | * |
2152 | * @name Paho.MQTT.Client#publish |
2153 | * @function |
2154 | * @param {string|Paho.MQTT.Message} topic - <b>mandatory</b> The name of the topic to which the message is to be published. |
2155 | * - If it is the only parameter, used as Paho.MQTT.Message object. |
2156 | * @param {String|ArrayBuffer} payload - The message data to be published. |
2157 | * @param {number} qos The Quality of Service used to deliver the message. |
2158 | * <dl> |
2159 | * <dt>0 Best effort (default). |
2160 | * <dt>1 At least once. |
2161 | * <dt>2 Exactly once. |
2162 | * </dl> |
2163 | * @param {Boolean} retained If true, the message is to be retained by the server and delivered |
2164 | * to both current and future subscriptions. |
2165 | * If false the server only delivers the message to current subscribers, this is the default for new Messages. |
2166 | * A received message has the retained boolean set to true if the message was published |
2167 | * with the retained boolean set to true |
2168 | * and the subscrption was made after the message has been published. |
2169 | * @throws {InvalidState} if the client is not connected. |
2170 | */ |
2171 | this.publish = function(topic,payload,qos,retained) { |
2172 | console.log("Publising message to: ", topic); |
2173 | var message ; |
2174 | |
2175 | if(arguments.length === 0){ |
2176 | throw new Error("Invalid argument."+"length"); |
2177 | |
2178 | }else if(arguments.length == 1) { |
2179 | |
2180 | if (!(topic instanceof Message) && (typeof topic !== "string")) |
2181 | throw new Error("Invalid argument:"+ typeof topic); |
2182 | |
2183 | message = topic; |
2184 | if (typeof message.destinationName === "undefined") |
2185 | throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"])); |
2186 | client.send(message); |
2187 | |
2188 | }else { |
2189 | //parameter checking in Message object |
2190 | message = new Message(payload); |
2191 | message.destinationName = topic; |
2192 | if(arguments.length >= 3) |
2193 | message.qos = qos; |
2194 | if(arguments.length >= 4) |
2195 | message.retained = retained; |
2196 | client.send(message); |
2197 | } |
2198 | }; |
2199 | |
2200 | /** |
2201 | * Normal disconnect of this Messaging client from its server. |
2202 | * |
2203 | * @name Paho.MQTT.Client#disconnect |
2204 | * @function |
2205 | * @throws {InvalidState} if the client is already disconnected. |
2206 | */ |
2207 | this.disconnect = function () { |
2208 | client.disconnect(); |
2209 | }; |
2210 | |
2211 | /** |
2212 | * Get the contents of the trace log. |
2213 | * |
2214 | * @name Paho.MQTT.Client#getTraceLog |
2215 | * @function |
2216 | * @return {Object[]} tracebuffer containing the time ordered trace records. |
2217 | */ |
2218 | this.getTraceLog = function () { |
2219 | return client.getTraceLog(); |
2220 | }; |
2221 | |
2222 | /** |
2223 | * Start tracing. |
2224 | * |
2225 | * @name Paho.MQTT.Client#startTrace |
2226 | * @function |
2227 | */ |
2228 | this.startTrace = function () { |
2229 | client.startTrace(); |
2230 | }; |
2231 | |
2232 | /** |
2233 | * Stop tracing. |
2234 | * |
2235 | * @name Paho.MQTT.Client#stopTrace |
2236 | * @function |
2237 | */ |
2238 | this.stopTrace = function () { |
2239 | client.stopTrace(); |
2240 | }; |
2241 | |
2242 | this.isConnected = function() { |
2243 | return client.connected; |
2244 | }; |
2245 | }; |
2246 | |
2247 | Client.prototype = { |
2248 | get host() { return this._getHost(); }, |
2249 | set host(newHost) { this._setHost(newHost); }, |
2250 | |
2251 | get port() { return this._getPort(); }, |
2252 | set port(newPort) { this._setPort(newPort); }, |
2253 | |
2254 | get path() { return this._getPath(); }, |
2255 | set path(newPath) { this._setPath(newPath); }, |
2256 | |
2257 | get clientId() { return this._getClientId(); }, |
2258 | set clientId(newClientId) { this._setClientId(newClientId); }, |
2259 | |
2260 | get onConnected() { return this._getOnConnected(); }, |
2261 | set onConnected(newOnConnected) { this._setOnConnected(newOnConnected); }, |
2262 | |
2263 | get disconnectedPublishing() { return this._getDisconnectedPublishing(); }, |
2264 | set disconnectedPublishing(newDisconnectedPublishing) { this._setDisconnectedPublishing(newDisconnectedPublishing); }, |
2265 | |
2266 | get disconnectedBufferSize() { return this._getDisconnectedBufferSize(); }, |
2267 | set disconnectedBufferSize(newDisconnectedBufferSize) { this._setDisconnectedBufferSize(newDisconnectedBufferSize); }, |
2268 | |
2269 | get onConnectionLost() { return this._getOnConnectionLost(); }, |
2270 | set onConnectionLost(newOnConnectionLost) { this._setOnConnectionLost(newOnConnectionLost); }, |
2271 | |
2272 | get onMessageDelivered() { return this._getOnMessageDelivered(); }, |
2273 | set onMessageDelivered(newOnMessageDelivered) { this._setOnMessageDelivered(newOnMessageDelivered); }, |
2274 | |
2275 | get onMessageArrived() { return this._getOnMessageArrived(); }, |
2276 | set onMessageArrived(newOnMessageArrived) { this._setOnMessageArrived(newOnMessageArrived); }, |
2277 | |
2278 | get trace() { return this._getTrace(); }, |
2279 | set trace(newTraceFunction) { this._setTrace(newTraceFunction); } |
2280 | |
2281 | }; |
2282 | |
2283 | /** |
2284 | * An application message, sent or received. |
2285 | * <p> |
2286 | * All attributes may be null, which implies the default values. |
2287 | * |
2288 | * @name Paho.MQTT.Message |
2289 | * @constructor |
2290 | * @param {String|ArrayBuffer} payload The message data to be sent. |
2291 | * <p> |
2292 | * @property {string} payloadString <i>read only</i> The payload as a string if the payload consists of valid UTF-8 characters. |
2293 | * @property {ArrayBuffer} payloadBytes <i>read only</i> The payload as an ArrayBuffer. |
2294 | * <p> |
2295 | * @property {string} destinationName <b>mandatory</b> The name of the destination to which the message is to be sent |
2296 | * (for messages about to be sent) or the name of the destination from which the message has been received. |
2297 | * (for messages received by the onMessage function). |
2298 | * <p> |
2299 | * @property {number} qos The Quality of Service used to deliver the message. |
2300 | * <dl> |
2301 | * <dt>0 Best effort (default). |
2302 | * <dt>1 At least once. |
2303 | * <dt>2 Exactly once. |
2304 | * </dl> |
2305 | * <p> |
2306 | * @property {Boolean} retained If true, the message is to be retained by the server and delivered |
2307 | * to both current and future subscriptions. |
2308 | * If false the server only delivers the message to current subscribers, this is the default for new Messages. |
2309 | * A received message has the retained boolean set to true if the message was published |
2310 | * with the retained boolean set to true |
2311 | * and the subscrption was made after the message has been published. |
2312 | * <p> |
2313 | * @property {Boolean} duplicate <i>read only</i> If true, this message might be a duplicate of one which has already been received. |
2314 | * This is only set on messages received from the server. |
2315 | * |
2316 | */ |
2317 | var Message = function (newPayload) { |
2318 | var payload; |
2319 | if ( typeof newPayload === "string" || |
2320 | newPayload instanceof ArrayBuffer || |
2321 | newPayload instanceof Int8Array || |
2322 | newPayload instanceof Uint8Array || |
2323 | newPayload instanceof Int16Array || |
2324 | newPayload instanceof Uint16Array || |
2325 | newPayload instanceof Int32Array || |
2326 | newPayload instanceof Uint32Array || |
2327 | newPayload instanceof Float32Array || |
2328 | newPayload instanceof Float64Array |
2329 | ) { |
2330 | payload = newPayload; |
2331 | } else { |
2332 | throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"])); |
2333 | } |
2334 | |
2335 | this._getPayloadString = function () { |
2336 | if (typeof payload === "string") |
2337 | return payload; |
2338 | else |
2339 | return parseUTF8(payload, 0, payload.length); |
2340 | }; |
2341 | |
2342 | this._getPayloadBytes = function() { |
2343 | if (typeof payload === "string") { |
2344 | var buffer = new ArrayBuffer(UTF8Length(payload)); |
2345 | var byteStream = new Uint8Array(buffer); |
2346 | stringToUTF8(payload, byteStream, 0); |
2347 | |
2348 | return byteStream; |
2349 | } else { |
2350 | return payload; |
2351 | } |
2352 | }; |
2353 | |
2354 | var destinationName; |
2355 | this._getDestinationName = function() { return destinationName; }; |
2356 | this._setDestinationName = function(newDestinationName) { |
2357 | if (typeof newDestinationName === "string") |
2358 | destinationName = newDestinationName; |
2359 | else |
2360 | throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"])); |
2361 | }; |
2362 | |
2363 | var qos = 0; |
2364 | this._getQos = function() { return qos; }; |
2365 | this._setQos = function(newQos) { |
2366 | if (newQos === 0 || newQos === 1 || newQos === 2 ) |
2367 | qos = newQos; |
2368 | else |
2369 | throw new Error("Invalid argument:"+newQos); |
2370 | }; |
2371 | |
2372 | var retained = false; |
2373 | this._getRetained = function() { return retained; }; |
2374 | this._setRetained = function(newRetained) { |
2375 | if (typeof newRetained === "boolean") |
2376 | retained = newRetained; |
2377 | else |
2378 | throw new Error(format(ERROR.INVALID_ARGUMENT, [newRetained, "newRetained"])); |
2379 | }; |
2380 | |
2381 | var duplicate = false; |
2382 | this._getDuplicate = function() { return duplicate; }; |
2383 | this._setDuplicate = function(newDuplicate) { duplicate = newDuplicate; }; |
2384 | }; |
2385 | |
2386 | Message.prototype = { |
2387 | get payloadString() { return this._getPayloadString(); }, |
2388 | get payloadBytes() { return this._getPayloadBytes(); }, |
2389 | |
2390 | get destinationName() { return this._getDestinationName(); }, |
2391 | set destinationName(newDestinationName) { this._setDestinationName(newDestinationName); }, |
2392 | |
2393 | get topic() { return this._getDestinationName(); }, |
2394 | set topic(newTopic) { this._setDestinationName(newTopic); }, |
2395 | |
2396 | get qos() { return this._getQos(); }, |
2397 | set qos(newQos) { this._setQos(newQos); }, |
2398 | |
2399 | get retained() { return this._getRetained(); }, |
2400 | set retained(newRetained) { this._setRetained(newRetained); }, |
2401 | |
2402 | get duplicate() { return this._getDuplicate(); }, |
2403 | set duplicate(newDuplicate) { this._setDuplicate(newDuplicate); } |
2404 | }; |
2405 | |
2406 | // Module contents. |
2407 | return { |
2408 | Client: Client, |
2409 | Message: Message |
2410 | }; |
2411 | })(window); |
2412 | return PahoMQTT; |
2413 | }); |
2414 |
1 | RewriteEngine On |
2 | |
3 | RewriteCond %{HTTP:X-Forwarded-Proto} !https |
4 | RewriteCond %{HTTPS} off |
5 | RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"} |
6 | RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] |
7 |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
1 | { |
2 | "compilerOptions": { |
3 | "checkJs": true, |
4 | "strictNullChecks": true, |
5 | "target": "ES6", |
6 | "module": "Node16", |
7 | "moduleResolution": "Node16", |
8 | "lib": [ |
9 | "ES2017", |
10 | "WebWorker", |
11 | "DOM" |
12 | ] |
13 | } |
14 | } |
En esta lección se presentó el proyecto base para IoT.
En esta lección se presentan programas para el NODE MCU ESP32 y como interactuar con el sistema de IoT.
1 | /* Este programa es un derivado de |
2 | ESP8266 Blink by Simon Peter */ |
3 | |
4 | /** Función que se invoca una sola |
5 | vez al inicio del programa. */ |
6 | void setup() { |
7 | /* Todas las placas compatibles |
8 | con Arduino incluyen un led |
9 | de prueba ligado a un pin |
10 | cuyo número está definido por |
11 | la constante LED_BUILTIN. |
12 | Inicializa el pin para |
13 | LED_BUILTIN como salida. */ |
14 | pinMode(LED_BUILTIN, OUTPUT); |
15 | } |
16 | |
17 | /* Función que se invoca |
18 | repetidamente mientras el |
19 | programa esté activo. */ |
20 | void loop() { |
21 | /* Enciende el LED_BUILTIN (HIGH |
22 | es el nivel de voltaje). */ |
23 | digitalWrite(LED_BUILTIN, HIGH); |
24 | delay(1000); // Espera 1 segundo |
25 | /* Apaga el LED_BUILTIN haciendo |
26 | que el nivel de voltaje sea |
27 | LOW. */ |
28 | digitalWrite(LED_BUILTIN, LOW); |
29 | delay(1000); |
30 | } |
31 |
1 | /* Este es un derivado de Button |
2 | por DojoDave y Tom Igoe */ |
3 | |
4 | /* Pin para el botón. */ |
5 | const int BOTON = 0; |
6 | |
7 | int estadoDelBoton = 0; |
8 | |
9 | void setup() { |
10 | /* Inicializa el pin para |
11 | LED_BUILTIN como salida. */ |
12 | pinMode(LED_BUILTIN, OUTPUT); |
13 | /* Initializa el pin para |
14 | BOTON como entrada. */ |
15 | pinMode(BOTON, INPUT); |
16 | } |
17 | |
18 | void loop() { |
19 | estadoDelBoton = |
20 | digitalRead(BOTON); |
21 | /* Checa si el botón está |
22 | presionado. Esto es, si |
23 | estadoDelBotón es LOW. */ |
24 | if (estadoDelBoton == LOW) { |
25 | // Enciende el LED_BUILTIN. |
26 | digitalWrite(LED_BUILTIN, HIGH); |
27 | } else { |
28 | // Apaga el LED_BUILTIN. |
29 | digitalWrite(LED_BUILTIN, LOW); |
30 | } |
31 | } |
32 |
En el siguiente código, sustituye la configuración por los datos de tu red WiFi.
La URL y el puerto deben coincidir con tu dervidor de MQTT. Si usas test.mosquitto.org, conserva los valores mostrados más adelante.
Para monitorear su funcionamiento, debes hacer clic en el botón de
arriba a la derecha en la ventana de Arduino, que tiene una lupa y
tiene el mensaje flotante que dice Monitor Serie.
Debes ajustar los baudios a la misma velocidad que en el código,
donde dice
Serial.begin(115200);
que en este caso es 115200 baudios. Si no se ven los mensajes,
baja la velocidad tanto en el código, como en el monitor.
Ve como configurar y ejecutar este ejemplo. No hagas caso a la parte de los certificados,
1 | #include "EspMQTTClient.h" |
2 | |
3 | const char *const SSID = |
4 | "Galaxy A723C85"; |
5 | const char *const PASS = |
6 | "bdoi1764"; |
7 | const char *const URL = |
8 | "test.mosquitto.org"; |
9 | const uint16_t PUERTO = |
10 | 1883; |
11 | const char *const CLIENT_ID = |
12 | "gilpgdmIoT-esp32-1"; |
13 | const char *const TOPICO_FOCO = |
14 | "gilpgdm/IoT/foco"; |
15 | |
16 | EspMQTTClient cliente( |
17 | SSID, |
18 | PASS, |
19 | URL, |
20 | 0, // Usuario opcional |
21 | 0, // Contraseña opcional |
22 | CLIENT_ID, |
23 | PUERTO); |
24 | const int BOTON = 0; |
25 | bool presionado = false; |
26 | String valor = "0"; |
27 | |
28 | void recibeMensaje( |
29 | const String &payload) |
30 | { |
31 | valor = payload; |
32 | digitalWrite(LED_BUILTIN, |
33 | valor == "1" |
34 | ? HIGH |
35 | : LOW); |
36 | } |
37 | |
38 | void enviaMensajeMqtt( |
39 | String valorAEnviar, |
40 | String topico) |
41 | { |
42 | cliente.publish(topico, |
43 | valorAEnviar); |
44 | } |
45 | |
46 | void onConnectionEstablished() |
47 | { |
48 | cliente.subscribe( |
49 | TOPICO_FOCO, recibeMensaje); |
50 | enviaMensajeMqtt( |
51 | valor, TOPICO_FOCO); |
52 | } |
53 | |
54 | void setup() |
55 | { |
56 | Serial.begin(115200); |
57 | pinMode(BOTON, INPUT); |
58 | pinMode(LED_BUILTIN, OUTPUT); |
59 | // Funcionalidades opcionaes |
60 | // Mensajes para depurar conexión |
61 | cliente.enableDebuggingMessages(); |
62 | cliente.enableHTTPWebUpdater(); |
63 | // Actualizaciones |
64 | // OTA (Over The Air) |
65 | cliente.enableOTA(); |
66 | // Mensaje de última voluntad. |
67 | cliente.enableLastWillMessage( |
68 | "gilpgdm/IoT/lastwill", |
69 | "Adios"); |
70 | } |
71 | |
72 | void loop() |
73 | { |
74 | cliente.loop(); |
75 | if (cliente.isConnected()) |
76 | { |
77 | bool actual = |
78 | digitalRead(BOTON); |
79 | if (!presionado && |
80 | actual == LOW) |
81 | { |
82 | enviaMensajeMqtt( |
83 | valor == "1" ? "0" : "1", |
84 | TOPICO_FOCO); |
85 | } |
86 | presionado = (actual == LOW); |
87 | } |
88 | } |
89 |
En esta lección se presentaron los siguientes archivos:
Blinkt.ino
Button.ino
Dispositivo.ino
En esta lección ejemplifica el funcionamiento de un sistema completo de IoT.
Aunque no se muestra, todas las páginsa están protegidas por un sistema de autenticación como en https://gilpgawoas.github.io/m19aut/ y no se pueden usar sin pasar por un inicio de desión.
En este ejemplo tendremos los siguientes usuarios y roles:
Los sistemas de IoT requieren recibir constantemente dinero. En este ejemplo, la financiación viene de estar pagando cada mes el servicio de alumbrado controlado por interruptores físicos y por un control remoto.
Para empezar, los clientes asisten a las oficinas de ventas de la empresa, firman un contrato y agendan una cita, pues en este caso el hacer funcionar este tipo de sistemas no es sencillo.
En este caso, la página está operada por el usuario cuca
, con el
rol Vandedor
, que recibe los datos proporcionados por los
contratantes.
El instalador asiste al domicilio del contratante y realiza la instalación del hardware y del software.
Se debe dejar funcionando todo el hardware, todo el software y la conexión al servidor.
En este caso, la instalación es realzada el usuario juan
, con el
rol Instalador
.
En teste caso, el sistema debe quedar operado por el usuario pepito
,
con el rol Cliente
. Si es necesario, debe recibir capacitación por
parte del instalador.
El proceso de instalación se parece a lo mostrado en este video: Configurar e instalar IoT.
El cliente, que en este caso es el usuario pepito
con el rol
Cliente
utiliza el sistema.
Se transmiten los datos de uso a la empresa de IoT. En este caso, los datos se envían a un servidor MQTT, que a su vez los reenvía a la empresa.
Pepito
pepito
169
Los datos enviados por MQTT se reciben en la empresa y se almacenan en la base de datos. Se puede usar un mecanismo parecido a la interfaz de uso, pero enviando los datos a un servicio que los almacene.
En este caso, el sistema es operado por el usuario angela
,
con el rol Operador
, que observa el comportamiento de la página y
revisa si hay fallos.
Botón presionado.
Id del dispositivo = 169.
Valor = 0.
Guardando valor en la base de datos.
Los datos de todos los dispositivos se almacenan en la tabla de histórico.
El campo timestamp representa hora y fecha en que se tomó la medición..
HISTORICO_ID | DISPOSITIVO_ID | HISTORICO_VALOR | HISTORICO_TIMESTAMP |
---|
Los datos en el sistema se utilizan para extraer conocimiento de los datos almacenados.
En este caso, el sistema es operado por el usuario perla
,
con el rol Cobranza
para generar la facturación.
Pepito
169
3000 minutos
$600.00
En esta lección ejemplificó el funcionamiento de un sistema completo de IoT
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" |
8 | content="width=device-width"> |
9 | |
10 | <title>Notificaciones</title> |
11 | |
12 | <style> |
13 | html { |
14 | color-scheme: light dark; |
15 | } |
16 | </style> |
17 | |
18 | </head> |
19 | |
20 | <body> |
21 | |
22 | <h1>Notificaciones</h1> |
23 | |
24 | <button type="button" |
25 | onclick="notifica()"> |
26 | Muestra |
27 | </button> |
28 | |
29 | <script> |
30 | |
31 | const MENSAJE = "Hola" |
32 | |
33 | async function notifica() { |
34 | let permitida = false |
35 | if ("Notification" in window) { |
36 | let permiso = |
37 | Notification.permission |
38 | if (permiso === "default") { |
39 | permiso = await Notification |
40 | .requestPermission() |
41 | } |
42 | permitida = |
43 | permiso === "granted" |
44 | ? true |
45 | : false |
46 | } |
47 | if (permitida) { |
48 | notificacion = |
49 | new Notification(MENSAJE) |
50 | } else { |
51 | alert(MENSAJE) |
52 | } |
53 | } |
54 | |
55 | </script> |
56 | |
57 | </body> |
58 | |
59 | </html> |
En esta lección se muestra un ejemplo de notificaciones push.
Puedes probar el ejemplo en https://notipush.rf.gd/.
Primero suscríbete y luego envía notificaciones a todos los que estén conectados.
Este ejercicio usa la librería WebPush para PHP. Puedes profundizar en este tema en la URL https://github.com/web-push-libs/web-push-php
Prueba el ejemplo en https://notipush.rf.gd/.
Descarga el archivo /src/notipush.zip y descompáctalo.
Crea tu proyecto en GitHub:
Crea una cuenta de email, por ejemplo, pepito@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.
Mantén la selección Public para que otros usuarios puedan ver tu proyecto.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.
Edita los archivos que desees.
El ejercicio usa una llave pública en el servidor y la repite el código de JavaScript en el navegador. También se requiere la correspondiente llave privada que se almacena solo en el servidor.
Las llaves se pueden generar con el enlace Genera llaves del ejemplo. Para copiar los valores despliega el código fuente de la página, porque en el navegador algunos caracteres pueden tener interpretación diferente.
El proyecto ya contiene la carpeta vendor
y el archivo
composer.lock
, pero es posible crearlos con estos pasos:
Instalar composer. Para Windows, usa el instalador de https://getcomposer.org/download/.
Abre una terminal y ejecuta el comando
composer update
Haz clic derecho en index.html
, selecciona
PHP Server: serve project y se abre el navegador para que puedas
probar localmente el ejemplo.
Para depurar paso a paso haz lo siguiente:
En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.
Recarga la página, de preferencia haciendo clic derecho en el ícono de volver a cargar la página y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página . Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.
Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).
Selecciona el archivo donde vas a empezar a depurar.
Haz clic en el número de la línea donde vas a empezar a depurar.
En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.
Haz clic en Run and Debug .
Si no está configurada la depuración, haz clic en create a launch json file.
Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .
Aparece un cuadro con los controles de depuración.
Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.
Regresa al navegador, recarga la página de manera normal y empieza a usarla.
Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.
Sube el proyecto al hosting que elijas sin incluir el archivo
.htaccess
. En algunos casos puedes usar
filezilla
(https://filezilla-project.org/)
En algunos host como InfinityFree, tienes que configurar el certificado SSL.
En algunos host, como InfinityFree, debes subir el
archivo .htaccess
cuando el certificado SSL se ha creado e
instalado. Sirve para forzar el uso de https, para eliminar el
control de cache, pues ahora lo lleva el service worker y para asignar el
mime type correcto para el archivo de manifest.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Haz clic en los triángulos para expandir las carpetas
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Notificaciones Push</title> |
10 | |
11 | </head> |
12 | |
13 | <body onload="preparaVista()"> |
14 | |
15 | <h1>Notificaciones Push</h1> |
16 | |
17 | <menu style="display: flex; list-style: none; flex-wrap: wrap; gap: 0.5rem;"> |
18 | <li> |
19 | <button id="btnSuscribe" type="button" hidden onclick="suscribe()"> |
20 | Suscríbete |
21 | </button> |
22 | </li> |
23 | <li> |
24 | <button id="btnCancela" type="button" hidden onclick="cancela()"> |
25 | Cancela suscripción |
26 | </button> |
27 | </li> |
28 | <li> |
29 | <button id="btnNotifica" type="button" hidden |
30 | onclick="notificaDesdeElServidor()"> |
31 | Notifica |
32 | </button> |
33 | </li> |
34 | <li> |
35 | <a href="srv/genera-llaves.php" target="_blank">Genera llaves</a> |
36 | </li> |
37 | </menu> |
38 | |
39 | <p> |
40 | <output id="outEstado"> |
41 | <progress max="100">Cargando…</progress> |
42 | </output> |
43 | </p> |
44 | |
45 | <fieldset> |
46 | <legend>Reporte de envío a endpoints</legend> |
47 | |
48 | <dl id="reporte"></dl> |
49 | |
50 | </fieldset> |
51 | |
52 | <script type="module"> |
53 | |
54 | import { exportaAHtml } from "./lib/js/exportaAHtml.js" |
55 | import { |
56 | activaNotificacionesPush |
57 | } from "./lib/js/activaNotificacionesPush.js" |
58 | import { getSuscripcionPush } from "./lib/js/getSuscripcionPush.js" |
59 | import { suscribeAPush } from "./lib/js/suscribeAPush.js" |
60 | import { cancelaSuscripcionPush } from "./lib/js/cancelaSuscripcionPush.js" |
61 | import { consumeJson } from "./lib/js/consumeJson.js" |
62 | import { enviaJson } from "./lib/js/enviaJson.js" |
63 | import { muestraError } from "./lib/js/muestraError.js" |
64 | import { muestraObjeto } from "./lib/js/muestraObjeto.js" |
65 | import { urlBase64ToUint8Array } from "./lib/js/urlBase64ToUint8Array.js" |
66 | import { |
67 | calculaDtoParaSuscripcion |
68 | } from "./lib/js/calculaDtoParaSuscripcion.js" |
69 | |
70 | const applicationServerKey = urlBase64ToUint8Array( |
71 | "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0") |
72 | /** @enum {string} */ |
73 | const Estado = { |
74 | CALCULANDO: "Calculando…", |
75 | SUSCRITO: "Suscrito", |
76 | DESUSCRITO: "Sin suscripción", |
77 | INCOMPATIBLE: "Incompatible" |
78 | } |
79 | |
80 | async function preparaVista() { |
81 | try { |
82 | await activaNotificacionesPush("sw.js") |
83 | const suscripcion = await getSuscripcionPush() |
84 | if (suscripcion === null) { |
85 | muestraEstado(Estado.DESUSCRITO) |
86 | } else { |
87 | // Modifica la suscripción en el servidor, |
88 | const dto = calculaDtoParaSuscripcion(suscripcion) |
89 | await enviaJson("srv/suscripcion-modifica.php", dto) |
90 | muestraEstado(Estado.SUSCRITO) |
91 | } |
92 | } catch (error) { |
93 | muestraEstado(Estado.INCOMPATIBLE) |
94 | muestraError(error) |
95 | } |
96 | } |
97 | exportaAHtml(preparaVista) |
98 | |
99 | async function notificaDesdeElServidor() { |
100 | try { |
101 | reporte.innerHTML = |
102 | /* html */ `<progress max="100">Cargando…</progress>` |
103 | const render = await consumeJson("srv/notifica.php") |
104 | await muestraObjeto(document, render.body) |
105 | } catch (error) { |
106 | muestraError(error) |
107 | } |
108 | } |
109 | exportaAHtml(notificaDesdeElServidor) |
110 | |
111 | async function suscribe() { |
112 | try { |
113 | muestraEstado(Estado.CALCULANDO) |
114 | const suscripcion = await suscribeAPush(applicationServerKey) |
115 | // Agrega la suscripción al servidor, |
116 | const dto = calculaDtoParaSuscripcion(suscripcion) |
117 | await enviaJson("srv/suscripcion-modifica.php", dto) |
118 | muestraEstado(Estado.SUSCRITO) |
119 | } catch (error) { |
120 | muestraError(error) |
121 | } |
122 | } |
123 | exportaAHtml(suscribe) |
124 | |
125 | async function cancela() { |
126 | try { |
127 | muestraEstado(Estado.CALCULANDO) |
128 | const suscripcion = await cancelaSuscripcionPush() |
129 | if (suscripcion !== null) { |
130 | // Elimina la suscripción en el servidor, |
131 | const dto = calculaDtoParaSuscripcion(suscripcion) |
132 | await enviaJson("srv/suscripcion-elimina.php", dto) |
133 | muestraEstado(Estado.DESUSCRITO) |
134 | } |
135 | } catch (error) { |
136 | muestraError(error) |
137 | } |
138 | } |
139 | exportaAHtml(cancela) |
140 | |
141 | /** @param {Estado} estado */ |
142 | function muestraEstado(estado) { |
143 | outEstado.value = estado |
144 | if (estado === Estado.INCOMPATIBLE || estado === Estado.CALCULANDO) { |
145 | btnSuscribe.hidden = true |
146 | btnCancela.hidden = true |
147 | btnNotifica.hidden = true |
148 | } else if (estado === Estado.SUSCRITO) { |
149 | btnSuscribe.hidden = true |
150 | btnCancela.hidden = false |
151 | btnNotifica.hidden = false |
152 | } else if (estado === Estado.DESUSCRITO) { |
153 | btnSuscribe.hidden = false |
154 | btnCancela.hidden = true |
155 | btnNotifica.hidden = true |
156 | } |
157 | } |
158 | |
159 | </script> |
160 | |
161 | </body> |
162 | |
163 | </html> |
1 | const URL_SERVIDOR = "https://notipush.rf.gd" |
2 | |
3 | if (self instanceof ServiceWorkerGlobalScope) { |
4 | |
5 | // El siguiente código se activa cuando llega una notificación push. |
6 | self.addEventListener("push", (/** @type {PushEvent} */ event) => { |
7 | const notificacion = event.data |
8 | /* Si el navegador tiene permitido mostrar notificaciones push, |
9 | * nuestra la que se ha recibido. */ |
10 | if (notificacion !== null && self.Notification.permission === 'granted') { |
11 | event.waitUntil(muestraNotificacion(notificacion)) |
12 | } |
13 | }) |
14 | |
15 | self.addEventListener("notificationclick", |
16 | (/** @type {NotificationEvent} */ event) => { |
17 | event.notification.close() |
18 | event.waitUntil(muestraVentana()) |
19 | }) |
20 | } |
21 | |
22 | /** |
23 | * @param {PushMessageData} notificacion |
24 | */ |
25 | async function muestraNotificacion(notificacion) { |
26 | if (self instanceof ServiceWorkerGlobalScope) { |
27 | const mensaje = notificacion.text() |
28 | await self.registration.showNotification(mensaje) |
29 | } |
30 | } |
31 | |
32 | async function muestraVentana() { |
33 | if (self instanceof ServiceWorkerGlobalScope) { |
34 | const clientes = await self.clients.matchAll({ type: "window" }) |
35 | for (const cliente of clientes) { |
36 | if (cliente.url.startsWith(URL_SERVIDOR)) { |
37 | return cliente.focus() |
38 | } |
39 | } |
40 | return self.clients.openWindow("/") |
41 | } |
42 | } |
1 | <?php |
2 | |
3 | class Bd |
4 | { |
5 | |
6 | private static ?PDO $pdo = null; |
7 | |
8 | static function pdo(): PDO |
9 | { |
10 | if (self::$pdo === null) { |
11 | |
12 | self::$pdo = new PDO( |
13 | // cadena de conexión |
14 | "sqlite:notificacionespush.db", |
15 | // usuario |
16 | null, |
17 | // contraseña |
18 | null, |
19 | // Opciones: pdos no persistentes y lanza excepciones. |
20 | [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] |
21 | ); |
22 | |
23 | self::$pdo->exec( |
24 | 'CREATE TABLE IF NOT EXISTS SUSCRIPCION ( |
25 | SUS_ENDPOINT TEXT NOT NULL, |
26 | SUS_PUB_KEY TEXT NOT NULL, |
27 | SUS_AUT_TOK TEXT NOT NULL, |
28 | SUS_CONT_ENCOD TEXT NOT NULL, |
29 | CONSTRAINT SUS_PK |
30 | PRIMARY KEY(SUS_ENDPOINT), |
31 | CONSTRAINT SUS_ENDPNT_NV |
32 | CHECK(LENGTH(SUS_ENDPOINT) > 0) |
33 | )' |
34 | ); |
35 | } |
36 | |
37 | return self::$pdo; |
38 | } |
39 | } |
40 |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Llaves VAPID</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Llaves VAPID</h1> |
16 | |
17 | <pre> |
18 | <?php |
19 | |
20 | require __DIR__ . "/../vendor/autoload.php"; |
21 | |
22 | use Minishlink\WebPush\VAPID; |
23 | |
24 | var_dump(VAPID::createVapidKeys()); |
25 | |
26 | ?> |
27 | </pre> |
28 | |
29 | </body> |
30 | |
31 | </html> |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../vendor/autoload.php"; |
4 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
5 | require_once __DIR__ . "/../lib/php/select.php"; |
6 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
7 | require_once __DIR__ . "/Bd.php"; |
8 | require_once __DIR__ . "/TABLA_SUSCRIPCION.php"; |
9 | require_once __DIR__ . "/Suscripcion.php"; |
10 | require_once __DIR__ . "/suscripcionElimina.php"; |
11 | |
12 | use Minishlink\WebPush\WebPush; |
13 | |
14 | const AUTH = [ |
15 | "VAPID" => [ |
16 | "subject" => "https://notificacionesphp.gilbertopachec2.repl.co/", |
17 | "publicKey" => "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0", |
18 | "privateKey" => "vplfkITvu0cwHqzK9Kj-DYStbCH_9AhGx9LqMyaeI6w" |
19 | ] |
20 | ]; |
21 | |
22 | ejecutaServicio(function () { |
23 | |
24 | $webPush = new WebPush(AUTH); |
25 | $mensaje = "Hola! 👋"; |
26 | |
27 | // Envia el mensaje a todas las suscripciones. |
28 | |
29 | $pdo = Bd::pdo(); |
30 | |
31 | $suscripciones = select( |
32 | pdo: $pdo, |
33 | from: SUSCRIPCION, |
34 | mode: PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, |
35 | opcional: Suscripcion::class |
36 | ); |
37 | |
38 | foreach ($suscripciones as $suscripcion) { |
39 | $webPush->queueNotification($suscripcion, $mensaje); |
40 | } |
41 | $reportes = $webPush->flush(); |
42 | |
43 | // Genera el reporte de envio a cada suscripcion. |
44 | $reporteDeEnvios = ""; |
45 | foreach ($reportes as $reporte) { |
46 | $endpoint = $reporte->getRequest()->getUri(); |
47 | $htmlEndpoint = htmlentities($endpoint); |
48 | if ($reporte->isSuccess()) { |
49 | // Reporte de éxito. |
50 | $reporteDeEnvios .= "<dt>$htmlEndpoint</dt><dd>Éxito</dd>"; |
51 | } else { |
52 | if ($reporte->isSubscriptionExpired()) { |
53 | suscripcionElimina($pdo, $endpoint); |
54 | } |
55 | // Reporte de fallo. |
56 | $explicacion = htmlentities($reporte->getReason()); |
57 | $reporteDeEnvios .= "<dt>$endpoint</dt><dd>Fallo: $explicacion</dd>"; |
58 | } |
59 | } |
60 | |
61 | devuelveJson(["reporte" => ["innerHTML" => $reporteDeEnvios]]); |
62 | }); |
63 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/recuperaJson.php"; |
5 | require_once __DIR__ . "/../lib/php/devuelveNoContent.php"; |
6 | require_once __DIR__ . "/Bd.php"; |
7 | require_once __DIR__ . "/suscripcionRecupera.php"; |
8 | require_once __DIR__ . "/suscripcionElimina.php"; |
9 | |
10 | ejecutaServicio(function () { |
11 | |
12 | $modelo = suscripcionRecupera(); |
13 | suscripcionElimina(Bd::pdo(), $modelo[SUS_ENDPOINT]); |
14 | devuelveNoContent(); |
15 | }); |
16 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/selectFirst.php"; |
5 | require_once __DIR__ . "/../lib/php/insert.php"; |
6 | require_once __DIR__ . "/../lib/php/update.php"; |
7 | require_once __DIR__ . "/../lib/php/devuelveCreated.php"; |
8 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
9 | require_once __DIR__ . "/Bd.php"; |
10 | require_once __DIR__ . "/Suscripcion.php"; |
11 | require_once __DIR__ . "/suscripcionRecupera.php"; |
12 | |
13 | ejecutaServicio(function () { |
14 | $modelo = suscripcionRecupera(); |
15 | $pdo = Bd::pdo(); |
16 | if ( |
17 | selectFirst($pdo, SUSCRIPCION, [SUS_ENDPOINT => $modelo[SUS_ENDPOINT]]) |
18 | === false |
19 | ) { |
20 | insert(pdo: $pdo, into: SUSCRIPCION, values: $modelo); |
21 | devuelveCreated("", $modelo); |
22 | } else { |
23 | update( |
24 | pdo: $pdo, |
25 | table: SUSCRIPCION, |
26 | set: $modelo, |
27 | where: [SUS_ENDPOINT => $modelo[SUS_ENDPOINT]] |
28 | ); |
29 | devuelveJson($modelo); |
30 | } |
31 | }); |
32 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../vendor/autoload.php"; |
4 | |
5 | use Minishlink\WebPush\SubscriptionInterface; |
6 | |
7 | class Suscripcion implements SubscriptionInterface |
8 | { |
9 | |
10 | public string $SUS_ENDPOINT; |
11 | public string $SUS_PUB_KEY; |
12 | public string $SUS_AUT_TOK; |
13 | public string $SUS_CONT_ENCOD; |
14 | |
15 | public function __construct( |
16 | string $SUS_ENDPOINT = "", |
17 | string $SUS_PUB_KEY = "", |
18 | string $SUS_AUT_TOK = "", |
19 | string $SUS_CONT_ENCOD = "" |
20 | ) { |
21 | $this->SUS_ENDPOINT = $SUS_ENDPOINT; |
22 | $this->SUS_PUB_KEY = $SUS_PUB_KEY; |
23 | $this->SUS_AUT_TOK = $SUS_AUT_TOK; |
24 | $this->SUS_CONT_ENCOD = $SUS_CONT_ENCOD; |
25 | } |
26 | |
27 | public function getEndpoint(): string |
28 | { |
29 | return $this->SUS_ENDPOINT; |
30 | } |
31 | |
32 | public function getPublicKey(): ?string |
33 | { |
34 | return $this->SUS_PUB_KEY; |
35 | } |
36 | |
37 | public function getAuthToken(): ?string |
38 | { |
39 | return $this->SUS_AUT_TOK; |
40 | } |
41 | |
42 | public function getContentEncoding(): ?string |
43 | { |
44 | return $this->SUS_CONT_ENCOD; |
45 | } |
46 | } |
47 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/delete.php"; |
4 | require_once __DIR__ . "/TABLA_SUSCRIPCION.php"; |
5 | |
6 | function suscripcionElimina(PDO $pdo, string $endpoint) |
7 | { |
8 | delete(pdo: $pdo, from: SUSCRIPCION, where: [SUS_ENDPOINT => $endpoint]); |
9 | } |
10 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/../lib/php/recuperaJson.php"; |
5 | require_once __DIR__ . "/../lib/php/validaJson.php"; |
6 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
7 | require_once __DIR__ . "/TABLA_SUSCRIPCION.php"; |
8 | |
9 | function suscripcionRecupera() |
10 | { |
11 | |
12 | $objeto = recuperaJson(); |
13 | $objeto = validaJson($objeto); |
14 | |
15 | if (!isset($objeto->publicKey) || !is_string($objeto->publicKey)) |
16 | throw new ProblemDetails( |
17 | type: "/error/publickeyincorrecta.html", |
18 | status: BAD_REQUEST, |
19 | title: "La publicKey debe ser texto.", |
20 | ); |
21 | |
22 | if (!isset($objeto->authToken) || !is_string($objeto->authToken)) |
23 | throw new ProblemDetails( |
24 | type: "/error/authtokenincorrecto.html", |
25 | status: BAD_REQUEST, |
26 | title: "El authToken debe ser texto.", |
27 | ); |
28 | |
29 | if (!isset($objeto->contentEncoding) || !is_string($objeto->contentEncoding)) |
30 | throw new ProblemDetails( |
31 | type: "/error/contentencodingincorrecta.html", |
32 | status: BAD_REQUEST, |
33 | title: "La contentEncoding debe ser texto.", |
34 | ); |
35 | |
36 | return [ |
37 | SUS_ENDPOINT => $objeto->endpoint, |
38 | SUS_PUB_KEY => $objeto->publicKey, |
39 | SUS_AUT_TOK => $objeto->authToken, |
40 | SUS_CONT_ENCOD => $objeto->contentEncoding |
41 | ]; |
42 | } |
43 |
1 | <?php |
2 | |
3 | const SUSCRIPCION = "SUSCRIPCION"; |
4 | const SUS_ENDPOINT = "SUS_ENDPOINT"; |
5 | const SUS_PUB_KEY = "SUS_PUB_KEY"; |
6 | const SUS_AUT_TOK = "SUS_AUT_TOK"; |
7 | const SUS_CONT_ENCOD = "SUS_CONT_ENCOD"; |
8 |
1 | { |
2 | "require": { |
3 | "minishlink/web-push": "^9.0.1" |
4 | } |
5 | } |
1 | -- No se muestra el contenido de este archivo -- |
1 | /** |
2 | * @param { string | URL } urlDeServiceWorkerQueRecibeNotificaciones |
3 | */ |
4 | export async function activaNotificacionesPush( |
5 | |
6 | urlDeServiceWorkerQueRecibeNotificaciones) { |
7 | |
8 | // Valida que el navegador soporte notificaciones push. |
9 | if (!('PushManager' in window)) |
10 | throw new Error("Este navegador no soporta notificaciones push.") |
11 | |
12 | // Valida que el navegador soporte notificaciones, |
13 | if (!("Notification" in window)) |
14 | throw new Error("Este navegador no soporta notificaciones push.") |
15 | |
16 | // Valida que el navegador soporte service workers, |
17 | if (!("serviceWorker" in navigator)) |
18 | throw new Error("Este navegador no soporta service workers.") |
19 | |
20 | // Recupera el permiso para usar notificaciones |
21 | let permiso = Notification.permission |
22 | if (permiso === "default") { |
23 | // Permiso no asignado. Pide al usuario su autorización. |
24 | permiso = await Notification.requestPermission() |
25 | } |
26 | |
27 | // Valida que el usuario haya permitido usar notificaciones.. |
28 | if (permiso === "denied") |
29 | throw new Error("Notificaciones bloqueadas.") |
30 | |
31 | const registro = await navigator.serviceWorker.register( |
32 | urlDeServiceWorkerQueRecibeNotificaciones) |
33 | console.log(urlDeServiceWorkerQueRecibeNotificaciones, "registrado.") |
34 | console.log(registro) |
35 | |
36 | if (!("showNotification" in registro)) |
37 | throw new Error("Este navegador no soporta notificaciones.") |
38 | } |
1 | /** |
2 | * Devuelve una literal de objeto que se puede usar para enviar |
3 | * en formato JSON al servidor. |
4 | * DTO es un acrónimo para Data Transder Object, u |
5 | * objeto para transferencia de datos. |
6 | * @param { PushSubscription } suscripcion |
7 | */ |
8 | export function calculaDtoParaSuscripcion(suscripcion) { |
9 | const key = suscripcion.getKey("p256dh") |
10 | const token = suscripcion.getKey("auth") |
11 | const supported = PushManager.supportedContentEncodings |
12 | const encodings = Array.isArray(supported) && supported.length > 0 |
13 | ? supported |
14 | : ["aesgcm"] |
15 | const endpoint = suscripcion.endpoint |
16 | const publicKey = key === null |
17 | ? null |
18 | : btoa(String.fromCharCode.apply(null, new Uint8Array(key))) |
19 | const authToken = token === null |
20 | ? null |
21 | : btoa(String.fromCharCode.apply(null, new Uint8Array(token))) |
22 | const contentEncoding = encodings[0] |
23 | return { |
24 | endpoint, |
25 | publicKey, |
26 | authToken, |
27 | contentEncoding |
28 | } |
29 | } |
30 |
1 | import { getSuscripcionPush } from "./getSuscripcionPush.js" |
2 | |
3 | export async function cancelaSuscripcionPush() { |
4 | const suscripcion = await getSuscripcionPush() |
5 | const resultado = suscripcion === null |
6 | ? false |
7 | : await suscripcion.unsubscribe() |
8 | return resultado === true ? suscripcion : null |
9 | } |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Espera a que la promesa de un fetch termine. Si |
6 | * hay error, lanza una excepción. Si no hay error, |
7 | * interpreta la respuesta del servidor como JSON y |
8 | * la convierte en una literal de objeto. |
9 | * |
10 | * @param { string | Promise<Response> } servicio |
11 | */ |
12 | export async function consumeJson(servicio) { |
13 | |
14 | if (typeof servicio === "string") { |
15 | servicio = fetch(servicio, { |
16 | headers: { "Accept": "application/json, application/problem+json" } |
17 | }) |
18 | } else if (!(servicio instanceof Promise)) { |
19 | throw new Error("Servicio de tipo incorrecto.") |
20 | } |
21 | |
22 | const respuesta = await servicio |
23 | |
24 | const headers = respuesta.headers |
25 | |
26 | if (respuesta.ok) { |
27 | // Aparentemente el servidor tuvo éxito. |
28 | |
29 | if (respuesta.status === 204) { |
30 | // No contiene texto de respuesta. |
31 | |
32 | return { headers, body: {} } |
33 | |
34 | } else { |
35 | |
36 | const texto = await respuesta.text() |
37 | |
38 | try { |
39 | |
40 | return { headers, body: JSON.parse(texto) } |
41 | |
42 | } catch (error) { |
43 | |
44 | // El contenido no es JSON. Probablemente sea texto de un error. |
45 | throw new ProblemDetails(respuesta.status, headers, texto, |
46 | "/error/errorinterno.html") |
47 | |
48 | } |
49 | |
50 | } |
51 | |
52 | } else { |
53 | // Hay un error. |
54 | |
55 | const texto = await respuesta.text() |
56 | |
57 | if (texto === "") { |
58 | |
59 | // No hay texto. Se usa el texto predeterminado. |
60 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText) |
61 | |
62 | } else { |
63 | // Debiera se un ProblemDetails en JSON. |
64 | |
65 | try { |
66 | |
67 | const { title, type, detail } = JSON.parse(texto) |
68 | |
69 | throw new ProblemDetails(respuesta.status, headers, |
70 | typeof title === "string" ? title : respuesta.statusText, |
71 | typeof type === "string" ? type : undefined, |
72 | typeof detail === "string" ? detail : undefined) |
73 | |
74 | } catch (error) { |
75 | |
76 | if (error instanceof ProblemDetails) { |
77 | // El error si era un ProblemDetails |
78 | |
79 | throw error |
80 | |
81 | } else { |
82 | |
83 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText, |
84 | undefined, texto) |
85 | |
86 | } |
87 | |
88 | } |
89 | |
90 | } |
91 | |
92 | } |
93 | |
94 | } |
95 | |
96 | exportaAHtml(consumeJson) |
1 | import { consumeJson } from "./consumeJson.js" |
2 | import { exportaAHtml } from "./exportaAHtml.js" |
3 | |
4 | /** |
5 | * @param { string } url |
6 | * @param { Object } body |
7 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
8 | * | "CONNECT" | "HEAD" } metodoHttp |
9 | */ |
10 | export async function enviaJson(url, body, metodoHttp = "POST") { |
11 | return await consumeJson(fetch(url, { |
12 | method: metodoHttp, |
13 | headers: { |
14 | "Content-Type": "application/json", |
15 | "Accept": "application/json, application/problem+json" |
16 | }, |
17 | body: JSON.stringify(body) |
18 | })) |
19 | } |
20 | |
21 | exportaAHtml(enviaJson) |
1 | /** |
2 | * Permite que los eventos de html usen la función. |
3 | * @param {function} functionInstance |
4 | */ |
5 | export function exportaAHtml(functionInstance) { |
6 | window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance |
7 | } |
8 | |
9 | /** |
10 | * @param {function} valor |
11 | */ |
12 | export function nombreDeFuncionParaHtml(valor) { |
13 | const names = valor.name.split(/\s+/g) |
14 | return names[names.length - 1] |
15 | } |
1 | export async function getSuscripcionPush() { |
2 | // Recupera el service worker registrado. |
3 | const registro = await navigator.serviceWorker.ready |
4 | return registro.pushManager.getSubscription() |
5 | } |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Muestra un error en la consola y en un cuadro de |
6 | * alerta el mensaje de una excepción. |
7 | * @param { ProblemDetails | Error | null } error descripción del error. |
8 | */ |
9 | export function muestraError(error) { |
10 | |
11 | if (error === null) { |
12 | |
13 | console.error("Error") |
14 | alert("Error") |
15 | |
16 | } else if (error instanceof ProblemDetails) { |
17 | |
18 | let mensaje = error.title |
19 | if (error.detail) { |
20 | mensaje += `\n\n${error.detail}` |
21 | } |
22 | mensaje += `\n\nCódigo: ${error.status}` |
23 | if (error.type) { |
24 | mensaje += ` ${error.type}` |
25 | } |
26 | |
27 | console.error(mensaje) |
28 | console.error(error) |
29 | console.error("Headers:") |
30 | error.headers.forEach((valor, llave) => console.error(llave, "=", valor)) |
31 | alert(mensaje) |
32 | |
33 | } else { |
34 | |
35 | console.error(error) |
36 | alert(error.message) |
37 | |
38 | } |
39 | |
40 | } |
41 | |
42 | exportaAHtml(muestraError) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | |
3 | /** |
4 | * @param { Document | HTMLElement } raizHtml |
5 | * @param { any } objeto |
6 | */ |
7 | export function muestraObjeto(raizHtml, objeto) { |
8 | |
9 | for (const [nombre, definiciones] of Object.entries(objeto)) { |
10 | |
11 | if (Array.isArray(definiciones)) { |
12 | |
13 | muestraArray(raizHtml, nombre, definiciones) |
14 | |
15 | } else if (definiciones !== undefined && definiciones !== null) { |
16 | |
17 | const elementoHtml = buscaElementoHtml(raizHtml, nombre) |
18 | |
19 | if (elementoHtml instanceof HTMLInputElement) { |
20 | |
21 | muestraInput(raizHtml, elementoHtml, definiciones) |
22 | |
23 | } else if (elementoHtml !== null) { |
24 | |
25 | for (const [atributo, valor] of Object.entries(definiciones)) { |
26 | if (atributo in elementoHtml) { |
27 | elementoHtml[atributo] = valor |
28 | } |
29 | } |
30 | |
31 | } |
32 | |
33 | } |
34 | |
35 | } |
36 | |
37 | } |
38 | exportaAHtml(muestraObjeto) |
39 | |
40 | /** |
41 | * @param { Document | HTMLElement } raizHtml |
42 | * @param { string } nombre |
43 | */ |
44 | export function buscaElementoHtml(raizHtml, nombre) { |
45 | return raizHtml.querySelector( |
46 | `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`) |
47 | } |
48 | |
49 | /** |
50 | * @param { Document | HTMLElement } raizHtml |
51 | * @param { string } propiedad |
52 | * @param {any[]} valores |
53 | */ |
54 | function muestraArray(raizHtml, propiedad, valores) { |
55 | |
56 | const conjunto = new Set(valores) |
57 | const elementos = |
58 | raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`) |
59 | |
60 | if (elementos.length === 1) { |
61 | const elemento = elementos[0] |
62 | |
63 | if (elemento instanceof HTMLSelectElement) { |
64 | const options = elemento.options |
65 | for (let i = 0, len = options.length; i < len; i++) { |
66 | const option = options[i] |
67 | option.selected = conjunto.has(option.value) |
68 | } |
69 | return |
70 | } |
71 | |
72 | } |
73 | |
74 | for (let i = 0, len = elementos.length; i < len; i++) { |
75 | const elemento = elementos[i] |
76 | if (elemento instanceof HTMLInputElement) { |
77 | elemento.checked = conjunto.has(elemento.value) |
78 | } |
79 | } |
80 | |
81 | } |
82 | |
83 | /** |
84 | * @param { Document | HTMLElement } raizHtml |
85 | * @param { HTMLInputElement } input |
86 | * @param { any } definiciones |
87 | */ |
88 | function muestraInput(raizHtml, input, definiciones) { |
89 | |
90 | for (const [atributo, valor] of Object.entries(definiciones)) { |
91 | |
92 | if (atributo == "data-file") { |
93 | |
94 | const img = getImgParaElementoHtml(raizHtml, input) |
95 | if (img !== null) { |
96 | input.dataset.file = valor |
97 | input.value = "" |
98 | if (valor === "") { |
99 | img.src = "" |
100 | img.hidden = true |
101 | } else { |
102 | img.src = valor |
103 | img.hidden = false |
104 | } |
105 | } |
106 | |
107 | } else if (atributo in input) { |
108 | |
109 | input[atributo] = valor |
110 | |
111 | } |
112 | } |
113 | |
114 | } |
115 | |
116 | /** |
117 | * @param { Document | HTMLElement } raizHtml |
118 | * @param { HTMLElement } elementoHtml |
119 | */ |
120 | export function getImgParaElementoHtml(raizHtml, elementoHtml) { |
121 | const imgId = elementoHtml.getAttribute("data-img") |
122 | if (imgId === null) { |
123 | return null |
124 | } else { |
125 | const input = buscaElementoHtml(raizHtml, imgId) |
126 | if (input instanceof HTMLImageElement) { |
127 | return input |
128 | } else { |
129 | return null |
130 | } |
131 | } |
132 | } |
1 | /** |
2 | * Detalle de los errores devueltos por un servicio. |
3 | */ |
4 | export class ProblemDetails extends Error { |
5 | |
6 | /** |
7 | * @param {number} status |
8 | * @param {Headers} headers |
9 | * @param {string} title |
10 | * @param {string} [type] |
11 | * @param {string} [detail] |
12 | */ |
13 | constructor(status, headers, title, type, detail) { |
14 | super(title) |
15 | /** |
16 | * @readonly |
17 | */ |
18 | this.status = status |
19 | /** |
20 | * @readonly |
21 | */ |
22 | this.headers = headers |
23 | /** |
24 | * @readonly |
25 | */ |
26 | this.type = type |
27 | /** |
28 | * @readonly |
29 | */ |
30 | this.detail = detail |
31 | /** |
32 | * @readonly |
33 | */ |
34 | this.title = title |
35 | } |
36 | |
37 | } |
1 | /** |
2 | * @param { Uint8Array } applicationServerKey |
3 | */ |
4 | export async function suscribeAPush(applicationServerKey) { |
5 | // Recupera el service worker registrado. |
6 | const registro = await navigator.serviceWorker.ready |
7 | return registro.pushManager.subscribe({ |
8 | userVisibleOnly: true, |
9 | applicationServerKey |
10 | }) |
11 | } |
1 | /** |
2 | * @param { string } base64String |
3 | */ |
4 | export function urlBase64ToUint8Array(base64String) { |
5 | const padding = "=".repeat((4 - (base64String.length % 4)) % 4) |
6 | const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/') |
7 | const rawData = atob(base64) |
8 | const outputArray = new Uint8Array(rawData.length) |
9 | for (let i = 0; i < rawData.length; ++i) { |
10 | outputArray[i] = rawData.charCodeAt(i) |
11 | } |
12 | return outputArray |
13 | } |
1 | <?php |
2 | |
3 | const BAD_REQUEST = 400; |
4 |
1 | <?php |
2 | |
3 | function calculaArregloDeParametros(array $arreglo) |
4 | { |
5 | $parametros = []; |
6 | foreach ($arreglo as $llave => $valor) { |
7 | $parametros[":$llave"] = $valor; |
8 | } |
9 | return $parametros; |
10 | } |
11 |
1 | <?php |
2 | |
3 | function calculaSqlDeAsignaciones(string $separador, array $arreglo) |
4 | { |
5 | $primerElemento = true; |
6 | $sqlDeAsignacion = ""; |
7 | foreach ($arreglo as $llave => $valor) { |
8 | $sqlDeAsignacion .= |
9 | ($primerElemento === true ? "" : $separador) . "$llave=:$llave"; |
10 | $primerElemento = false; |
11 | } |
12 | return $sqlDeAsignacion; |
13 | } |
14 |
1 | <?php |
2 | |
3 | function calculaSqlDeCamposDeInsert(array $values) |
4 | { |
5 | $primerCampo = true; |
6 | $sqlDeCampos = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeCampos .= ($primerCampo === true ? "" : ",") . "$nombreDeValue"; |
9 | $primerCampo = false; |
10 | } |
11 | return $sqlDeCampos; |
12 | } |
13 |
1 | <?php |
2 | |
3 | function calculaSqlDeValues(array $values) |
4 | { |
5 | $primerValue = true; |
6 | $sqlDeValues = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeValues .= ($primerValue === true ? "" : ",") . ":$nombreDeValue"; |
9 | $primerValue = false; |
10 | } |
11 | return $sqlDeValues; |
12 | } |
13 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | function delete(PDO $pdo, string $from, array $where) |
7 | { |
8 | $sql = "DELETE FROM $from"; |
9 | |
10 | if (sizeof($where) === 0) { |
11 | $pdo->exec($sql); |
12 | } else { |
13 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
14 | $sql .= " WHERE $sqlDeWhere"; |
15 | |
16 | $statement = $pdo->prepare($sql); |
17 | $parametros = calculaArregloDeParametros($where); |
18 | $statement->execute($parametros); |
19 | } |
20 | } |
21 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | |
5 | function devuelveCreated($urlDelNuevo, $resultado) |
6 | { |
7 | |
8 | $json = json_encode($resultado); |
9 | |
10 | if ($json === false) { |
11 | |
12 | devuelveResultadoNoJson(); |
13 | } else { |
14 | |
15 | http_response_code(201); |
16 | header("Location: {$urlDelNuevo}"); |
17 | header("Content-Type: application/json"); |
18 | echo $json; |
19 | } |
20 | } |
21 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
6 | |
7 | function devuelveErrorInterno(Throwable $error) |
8 | { |
9 | devuelveProblemDetails(new ProblemDetails( |
10 | status: INTERNAL_SERVER_ERROR, |
11 | title: $error->getMessage(), |
12 | type: "/error/errorinterno.html" |
13 | )); |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | |
5 | function devuelveJson($resultado) |
6 | { |
7 | |
8 | $json = json_encode($resultado); |
9 | |
10 | if ($json === false) { |
11 | |
12 | devuelveResultadoNoJson(); |
13 | } else { |
14 | |
15 | http_response_code(200); |
16 | header("Content-Type: application/json"); |
17 | echo $json; |
18 | } |
19 | } |
20 |
1 | <?php |
2 | |
3 | function devuelveNoContent() |
4 | { |
5 | http_response_code(204); |
6 | } |
7 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function devuelveProblemDetails(ProblemDetails $details) |
7 | { |
8 | |
9 | $body = ["title" => $details->title]; |
10 | if ($details->type !== null) { |
11 | $body["type"] = $details->type; |
12 | } |
13 | if ($details->detail !== null) { |
14 | $body["detail"] = $details->detail; |
15 | } |
16 | |
17 | $json = json_encode($body); |
18 | |
19 | if ($json === false) { |
20 | |
21 | devuelveResultadoNoJson(); |
22 | } else { |
23 | |
24 | http_response_code($details->status); |
25 | header("Content-Type: application/problem+json"); |
26 | echo $json; |
27 | } |
28 | } |
29 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | |
5 | function devuelveResultadoNoJson() |
6 | { |
7 | |
8 | http_response_code(INTERNAL_SERVER_ERROR); |
9 | header("Content-Type: application/problem+json"); |
10 | echo '{' . |
11 | '"title": "El resultado no puede representarse como JSON."' . |
12 | '"type": "/error/resultadonojson.html"' . |
13 | '}'; |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveErrorInterno.php"; |
6 | |
7 | function ejecutaServicio(callable $codigo) |
8 | { |
9 | try { |
10 | $codigo(); |
11 | } catch (ProblemDetails $details) { |
12 | devuelveProblemDetails($details); |
13 | } catch (Throwable $error) { |
14 | devuelveErrorInterno($error); |
15 | } |
16 | } |
17 |
1 | <?php |
2 | |
3 | function fetch( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ) { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return false; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | if ($opcional === null) { |
20 | return $statement->fetch($mode); |
21 | } else { |
22 | $statement->setFetchMode($mode, $opcional); |
23 | return $statement->fetch(); |
24 | } |
25 | } |
26 | } |
27 |
1 | <?php |
2 | |
3 | function fetchAll( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ): array { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return []; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | $resultado = $opcional === null |
20 | ? $statement->fetchAll($mode) |
21 | : $statement->fetchAll($mode, $opcional); |
22 | |
23 | if ($resultado === false) { |
24 | return []; |
25 | } else { |
26 | return $resultado; |
27 | } |
28 | } |
29 | } |
30 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaSqlDeCamposDeInsert.php"; |
4 | require_once __DIR__ . "/calculaSqlDeValues.php"; |
5 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
6 | |
7 | function insert(PDO $pdo, string $into, array $values) |
8 | { |
9 | $sqlDeCampos = calculaSqlDeCamposDeInsert($values); |
10 | $sqlDeValues = calculaSqlDeValues($values); |
11 | $sql = "INSERT INTO $into ($sqlDeCampos) VALUES ($sqlDeValues)"; |
12 | $parametros = calculaArregloDeParametros($values); |
13 | $pdo->prepare($sql)->execute($parametros); |
14 | } |
15 |
1 | <?php |
2 | |
3 | const INTERNAL_SERVER_ERROR = 500; |
1 | <?php |
2 | |
3 | /** Detalle de los errores devueltos por un servicio. */ |
4 | class ProblemDetails extends Exception |
5 | { |
6 | |
7 | public int $status; |
8 | public string $title; |
9 | public ?string $type; |
10 | public ?string $detail; |
11 | |
12 | public function __construct( |
13 | int $status, |
14 | string $title, |
15 | ?string $type = null, |
16 | ?string $detail = null, |
17 | Throwable $previous = null |
18 | ) { |
19 | parent::__construct($title, $status, $previous); |
20 | $this->status = $status; |
21 | $this->type = $type; |
22 | $this->title = $title; |
23 | $this->detail = $detail; |
24 | } |
25 | } |
26 |
1 | <?php |
2 | |
3 | function recuperaJson() |
4 | { |
5 | return json_decode(file_get_contents("php://input")); |
6 | } |
7 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetchAll.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | function select( |
7 | PDO $pdo, |
8 | string $from, |
9 | array $where = [], |
10 | string $orderBy = "", |
11 | int $mode = PDO::FETCH_ASSOC, |
12 | $opcional = null |
13 | ) { |
14 | $sql = "SELECT * FROM $from"; |
15 | |
16 | if (sizeof($where) > 0) { |
17 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
18 | $sql .= " WHERE $sqlDeWhere"; |
19 | } |
20 | |
21 | if ($orderBy !== "") { |
22 | $sql .= " ORDER BY $orderBy"; |
23 | } |
24 | |
25 | if (sizeof($where) === 0) { |
26 | $statement = $pdo->query($sql); |
27 | return fetchAll($statement, [], $mode, $opcional); |
28 | } else { |
29 | $statement = $pdo->prepare($sql); |
30 | $parametros = calculaArregloDeParametros($where); |
31 | return fetchAll($statement, $parametros, $mode, $opcional); |
32 | } |
33 | } |
34 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetch.php"; |
4 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
5 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
6 | |
7 | function selectFirst( |
8 | PDO $pdo, |
9 | string $from, |
10 | array $where = [], |
11 | string $orderBy = "", |
12 | int $mode = PDO::FETCH_ASSOC, |
13 | $opcional = null |
14 | ) { |
15 | $sql = "SELECT * FROM $from"; |
16 | |
17 | if (sizeof($where) > 0) { |
18 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
19 | $sql .= " WHERE $sqlDeWhere"; |
20 | } |
21 | |
22 | if ($orderBy !== "") { |
23 | $sql .= " ORDER BY $orderBy"; |
24 | } |
25 | |
26 | if (sizeof($where) === 0) { |
27 | $statement = $pdo->query($sql); |
28 | return fetch($statement, [], $mode, $opcional); |
29 | } else { |
30 | $statement = $pdo->prepare($sql); |
31 | $parametros = calculaArregloDeParametros($where); |
32 | return fetch($statement, $parametros, $mode, $opcional); |
33 | } |
34 | } |
35 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | |
7 | function update(PDO $pdo, string $table, array $set, array $where) |
8 | { |
9 | $sqlDeSet = calculaSqlDeAsignaciones(",", $set); |
10 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
11 | $sql = "UPDATE $table SET $sqlDeSet WHERE $sqlDeWhere"; |
12 | |
13 | $parametros = calculaArregloDeParametros($set); |
14 | foreach ($where as $nombreDeWhere => $valorDeWhere) { |
15 | $parametros[":$nombreDeWhere"] = $valorDeWhere; |
16 | } |
17 | $statement = $pdo->prepare($sql); |
18 | $statement->execute($parametros); |
19 | } |
20 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function validaJson($objeto) |
7 | { |
8 | |
9 | if ($objeto === null) |
10 | throw new ProblemDetails( |
11 | status: BAD_REQUEST, |
12 | title: "Los datos recibidos no son JSON.", |
13 | type: "/error/datosnojson.html", |
14 | detail: "Los datos recibidos no están en formato JSON.O", |
15 | ); |
16 | |
17 | return $objeto; |
18 | } |
19 |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>El authToken debe ser texto</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>El authToken debe ser texto</h1> |
14 | |
15 | </body> |
16 | |
17 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>La contentEncoding debe ser texto</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>La contentEncoding debe ser texto</h1> |
14 | |
15 | </body> |
16 | |
17 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Los datos recibidos no son JSON</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Los datos recibidos no son JSON</h1> |
16 | |
17 | <p> |
18 | Los datos recibidos no están en formato JSON. |
19 | </p> |
20 | |
21 | </body> |
22 | |
23 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>El endpoint debe ser texto</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>El endpoint debe ser texto</h1> |
14 | |
15 | </body> |
16 | |
17 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Error interno del servidor</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Error interno del servidor</h1> |
16 | |
17 | <p>Se presentó de forma inesperada un error interno del servidor.</p> |
18 | |
19 | </body> |
20 | |
21 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>La publicKey debe ser texto</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>La publicKey debe ser texto</h1> |
14 | |
15 | </body> |
16 | |
17 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>El resultado no puede representarse como JSON</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>El resultado no puede representarse como JSON</h1> |
16 | |
17 | <p> |
18 | Debido a un error interno del servidor, el resultado generado, no se puede |
19 | recuperar. |
20 | </p> |
21 | |
22 | </body> |
23 | |
24 | </html> |
1 |
1 | RewriteEngine On |
2 | |
3 | RewriteCond %{HTTP:X-Forwarded-Proto} !https |
4 | RewriteCond %{HTTPS} off |
5 | RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"} |
6 | RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] |
7 |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
1 | { |
2 | "compilerOptions": { |
3 | "checkJs": true, |
4 | "strictNullChecks": true, |
5 | "target": "ES6", |
6 | "module": "Node16", |
7 | "moduleResolution": "Node16", |
8 | "lib": [ |
9 | "ES2017", |
10 | "WebWorker", |
11 | "DOM" |
12 | ] |
13 | } |
14 | } |
En esta lección se muestró un ejemplo de notificaciones push.
En esta lección se muestra un ejemplo de sincronización de bases de datos.
Puedes probar el ejemplo en varios navegadores y dispositivos abriendo la url https://sincro.rf.gd/.
Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.
Puedes trabajar sin conexión en algunos dispositivos y con conexión en otros. Si conectas todos los dispositivos, estos mostrarán los mismos datos después de un tiempo.
Comercialmente hay algunos productos como:
Prueba el ejemplo en https://sincro.rf.gd/.
Copia la url de la app y pégala en varios navegadores y dispositivos.
Las modificaciones que realices en dispositivo o navegador se verán reflejados en los otros dispositivos, en un máximo de 20 segundos.
Puedes trabajar sin conexión en algunos dispositivos y con conexión en otros. Si conectas todos los dispositivos, estos mostrarán los mismos datos después de un tiempo.
Descarga el archivo /src/sincro.zip y descompáctalo.
Crea tu proyecto en GitHub:
Crea una cuenta de email, por ejemplo, pepito@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.
Mantén la selección Public para que otros usuarios puedan ver tu proyecto.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.
Edita los archivos que desees.
El archivo sw.js
tiene una lista de los archivos que se instalan.
El archivo instruccionesListadoSw.txt
te indica como generarla usando
Visual Studio Code.
Cada vez que modifiques los archivos, debes modificar el valor
de VERSION en el archivo sw.js
para poder ver los cambios
en el navegador.
Haz clic derecho en index.html
, selecciona
PHP Server: serve project y se abre el navegador para que puedas
probar localmente el ejemplo.
Cuando desarrolles, es incómodo modificar la versión cada que realizas cambios; en ves de ello desinstala la app:
Abre las herramientas de depuración haciendo clic derecho en la página y selecciona Inspeccionar (o Inspect si aparece en inglés).
En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamoento (o Storage en inglés). Cliquea Borrar datos del sitio.
Recarga la app, de preferencia haciendo clic derecho en el ícono de volver a cargar la página y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página . Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.
Tanbién puedes usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice temporalmente el navegador en caso de que no se vean los cambios.
En la Pestaña Aplicación (o Application en inglés) selecciona Almacenamiento en caché (o Cache storage en inglés). Aquí puedes revisar si el caché de la aplicación se llenó correctamente. En caso de que esté vacío, es que hubo algún error durante la carga y la app se ejecuta más lenta.
Para depurar paso a paso haz lo siguiente:
En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.
Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).
Selecciona el archivo donde vas a empezar a depurar.
Haz clic en el número de la línea donde vas a empezar a depurar.
En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.
Haz clic en Run and Debug .
Si no está configurada la depuración, haz clic en create a launch json file.
Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .
Aparece un cuadro con los controles de depuración
Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.
Regresa al navegador, recarga la página de manera normal y empieza a usarla.
Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.
Sube el proyecto al hosting que elijas sin incluir el archivo
.htaccess
. En algunos casos puedes usar
filezilla
(https://filezilla-project.org/)
En algunos host como InfinityFree, tienes que configurar el certificado SSL.
En algunos host, como InfinityFree, debes subir el
archivo .htaccess
cuando el certificado SSL se ha creado e
instalado. Sirve para forzar el uso de https, para eliminar el
control de cache, pues ahora lo lleva el service worker y para asignar el
mime type correcto para el archivo de manifest.
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Haz clic en los triángulos para expandir las carpetas
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | |
8 | <title>Sincronizacion</title> |
9 | |
10 | <meta name="viewport" content="width=device-width"> |
11 | |
12 | <script src="js/registraServiceWorker.js"></script> |
13 | <script type="module" src="lib/js/muestraError.js"></script> |
14 | <script type="module" src="js/bd/pasatiempoConsultaNoEliminados.js"></script> |
15 | <script type="module" src="js/renderiza.js"></script> |
16 | <script type="module" src="js/sincroniza.js"></script> |
17 | <script type="module" src="js/esperaUnPocoYSincroniza.js"></script> |
18 | |
19 | </head> |
20 | |
21 | <body onload="pasatiempoConsultaNoEliminados() |
22 | .then(pasatiempos => { |
23 | renderiza(lista, pasatiempos) |
24 | sincroniza(lista) |
25 | }) |
26 | .catch(error => { |
27 | muestraError(error) |
28 | esperaUnPocoYSincroniza(lista) |
29 | })"> |
30 | |
31 | <h1>Sincronizacion</h1> |
32 | |
33 | <p><a href="agrega.html">Agregar</a></p> |
34 | |
35 | <ul id="lista"> |
36 | <li><progress max="100">Cargando…</progress></li> |
37 | </ul> |
38 | |
39 | </body> |
40 | |
41 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Agregar</title> |
10 | |
11 | <script type="module" src="js/configura.js"></script> |
12 | <script type="module" src="lib/js/muestraError.js"></script> |
13 | <script type="module" src="js/bd/pasatiempoAgrega.js"></script> |
14 | |
15 | </head> |
16 | |
17 | <body> |
18 | |
19 | <form id="forma" onsubmit=" |
20 | event.preventDefault() |
21 | // Lee el nombre, quitándole los espacios al inicio y al final. |
22 | const PAS_NOMBRE = forma.nombre.value.trim() |
23 | const modelo = { PAS_NOMBRE } |
24 | pasatiempoAgrega(modelo) |
25 | .then(json => location.href = 'index.html') |
26 | .catch(muestraError)"> |
27 | |
28 | <h1>Agregar</h1> |
29 | |
30 | <p><a href="index.html">Cancelar</a></p> |
31 | |
32 | <p> |
33 | <label> |
34 | Nombre * |
35 | <input name="nombre"> |
36 | </label> |
37 | </p> |
38 | <p>* Obligatorio</p> |
39 | <p><button type="submit">Agregar</button></p> |
40 | |
41 | </form> |
42 | |
43 | </body> |
44 | |
45 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Modificar</title> |
10 | |
11 | <script type="module" src="js/configura.js"></script> |
12 | <script type="module" src="lib/js/muestraError.js"></script> |
13 | <script type="module" src="lib/js/muestraObjeto.js"></script> |
14 | <script type="module" src="js/bd/pasatiempoBusca.js"></script> |
15 | <script type="module" src="js/bd/pasatiempoElimina.js"></script> |
16 | <script type="module" src="js/bd/pasatiempoModifica.js"></script> |
17 | |
18 | <script> |
19 | |
20 | // Obtiene los parámetros de la página. |
21 | const parametros = new URL(location.href).searchParams |
22 | |
23 | const paramId = parametros.get("id") |
24 | |
25 | </script> |
26 | |
27 | </head> |
28 | |
29 | <body onload="if (paramId !== null) { |
30 | pasatiempoBusca(paramId) |
31 | .then(pasatiempo => { |
32 | if (pasatiempo === undefined) throw new Error('Pasatiempo no encontrado.') |
33 | muestraObjeto(forma, { nombre: { value: pasatiempo.PAS_NOMBRE } }) |
34 | }) |
35 | .catch(muestraError) |
36 | }"> |
37 | |
38 | <form id="forma" onsubmit=" |
39 | event.preventDefault() |
40 | if (paramId !== null) { |
41 | const PAS_ID = paramId |
42 | // Lee el nombre, quitándole los espacios al inicio y al final. |
43 | const PAS_NOMBRE = forma.nombre.value.trim() |
44 | const modelo = { PAS_ID, PAS_NOMBRE } |
45 | pasatiempoModifica(modelo) |
46 | .then(json => location.href = 'index.html') |
47 | .catch(muestraError) |
48 | }"> |
49 | |
50 | <h1>Modificar</h1> |
51 | |
52 | <p><a href="index.html">Cancelar</a></p> |
53 | |
54 | <p> |
55 | <label> |
56 | Nombre * |
57 | <input name="nombre" value="Cargando…"> |
58 | </label> |
59 | </p> |
60 | |
61 | <p>* Obligatorio</p> |
62 | |
63 | <p> |
64 | |
65 | <button type="submit">Guardar</button> |
66 | |
67 | <button type="button" onclick=" |
68 | if (paramId !== null && confirm('Confirma la eliminación')) { |
69 | pasatiempoElimina(paramId) |
70 | .then(() => location.href = 'index.html') |
71 | .catch(muestraError) |
72 | }"> |
73 | Eliminar |
74 | </button> |
75 | |
76 | </p> |
77 | |
78 | </form> |
79 | |
80 | </body> |
81 | |
82 | </html> |
1 | Generar el listado de archivos del sw.js desde Visual Studio Code. |
2 | 1. Abrir una terminal desde el menú |
3 | Terminal > New Terminal |
4 | |
5 | 2. Desde la terminal introducir la orden: |
6 | Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt |
7 | |
8 | 3. Abrir el archivo generado, que se llama |
9 | archivos.txt |
10 | y sobre este, realizar los pasos que siguen: |
11 | |
12 | 4. Quita del archivo archivos.txt: |
13 | * el encabezado, |
14 | * todas las carpetas, |
15 | * el archivo .vscode/settings.json, |
16 | * el archivo .htaccess, |
17 | * el archivo archivos.txt, |
18 | * este archivo (instruccionesListadoSw.txt), |
19 | * el archivo jsconfig.json, |
20 | * el archivo sw.js, |
21 | * el archivo de la base de datos, que termina en ".bd" y |
22 | está en la carpeta srv, |
23 | * todos los archivos de php y |
24 | * las líneas en blanco del final |
25 | |
26 | 5. Cambia los \ por / desde Visual Studio Code con las siguientes |
27 | combinaciones de teclas: |
28 | |
29 | Ctrl+H En el diálogo que aparece introduce lo siguiente: |
30 | Find:\ |
31 | Replace:/ |
32 | |
33 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
34 | |
35 | 6. Coloca las comillas y coma del final de cada línea desde Visual |
36 | Studio Code con las siguientes combinaciones de teclas: |
37 | |
38 | Ctrl+H En el diálogo que aparece, selecciona el botón |
39 | ".*" |
40 | e introduce lo siguiente: |
41 | Find:\s*$ |
42 | Replace:", |
43 | |
44 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
45 | |
46 | 7. Marca la carpeta inicial, presiona la combinación de teclas: |
47 | |
48 | Shift+Ctrl+L |
49 | |
50 | borra la selección, teclea " y luego ESC |
51 | |
52 | 8. Cambia las secuencias de espacios por / con las siguientes |
53 | combinaciones de teclas: |
54 | |
55 | Ctrl+H En el diálogo que aparece, selecciona el botón |
56 | ".*" |
57 | e introduce lo siguiente: |
58 | Find:\s+ |
59 | Replace:/ |
60 | |
61 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
62 | |
63 | 9. Cambia las "/ por " con las siguientes combinaciones de teclas: |
64 | |
65 | Ctrl+H En el diálogo que aparece, quita la selección del botón |
66 | ".*" |
67 | e introduce lo siguiente: |
68 | Find:"/ |
69 | Replace:" |
70 | |
71 | Clic en el icono Reemplaza todo o Replace All y luego teclea ESC |
72 | |
73 | 10. Copia el texto al archivo |
74 | sw.js |
75 | en el contenido del arreglo llamado ARCHIVOS, pero recuerda |
76 | mantener el último elemento, que dice: |
77 | "/" |
1 | "agrega.html", |
2 | "index.html", |
3 | "modifica.html", |
4 | "error/datosnojson.html", |
5 | "error/eliminadoincorrecto.html", |
6 | "error/errorinterno.html", |
7 | "error/faltaid.html", |
8 | "error/faltanombre.html", |
9 | "error/idincorrecto.html", |
10 | "error/modificacionincorrecta.html", |
11 | "error/nombreenblanco.html", |
12 | "error/nombreincorrecto.html", |
13 | "error/resultadonojson.html", |
14 | "js/esperaUnPocoYSincroniza.js", |
15 | "js/registraServiceWorker.js", |
16 | "js/renderiza.js", |
17 | "js/sincroniza.js", |
18 | "js/bd/Bd.js", |
19 | "js/bd/pasatiempoAgrega.js", |
20 | "js/bd/pasatiempoBusca.js", |
21 | "js/bd/pasatiempoConsultaNoEliminados.js", |
22 | "js/bd/pasatiempoConsultaTodos.js", |
23 | "js/bd/pasatiempoElimina.js", |
24 | "js/bd/pasatiempoModifica.js", |
25 | "js/bd/pasatiemposReemplaza.js", |
26 | "js/modelo/PASATIEMPO.js", |
27 | "js/modelo/validaId.js", |
28 | "js/modelo/validaNombre.js", |
29 | "js/modelo/validaPasatiempo.js", |
30 | "js/modelo/validaPasatiempos.js", |
31 | "lib/js/bdConsulta.js", |
32 | "lib/js/bdEjecuta.js", |
33 | "lib/js/consumeJson.js", |
34 | "lib/js/creaIdCliente.js", |
35 | "lib/js/enviaJson.js", |
36 | "lib/js/exportaAHtml.js", |
37 | "lib/js/htmlentities.js", |
38 | "lib/js/muestraError.js", |
39 | "lib/js/muestraObjeto.js", |
40 | "lib/js/ProblemDetails.js", |
1 | /* Este archivo debe estar colocado en la carpeta raíz del sitio. |
2 | * |
3 | * Cualquier cambio en el contenido de este archivo hace que el service |
4 | * worker se reinstale. */ |
5 | |
6 | /** |
7 | * Cambia el número de la versión cuando cambia el contenido de los |
8 | * archivos. |
9 | * |
10 | * El número a la izquierda del punto (.), en este caso <q>1</q>, se |
11 | * conoce como número mayor y se cambia cuando se realizan |
12 | * modificaciones grandes o importantes. |
13 | * |
14 | * El número a la derecha del punto (.), en este caso <q>00</q>, se |
15 | * conoce como número menor y se cambia cuando se realizan |
16 | * modificaciones menores. |
17 | */ |
18 | const VERSION = "1.00" |
19 | |
20 | /** |
21 | * Nombre de la carpeta de caché. |
22 | */ |
23 | const CACHE = "sincro" |
24 | |
25 | /** |
26 | * Archivos requeridos para que la aplicación funcione fuera de línea. |
27 | */ |
28 | const ARCHIVOS = [ |
29 | "agrega.html", |
30 | "index.html", |
31 | "modifica.html", |
32 | "error/datosnojson.html", |
33 | "error/eliminadoincorrecto.html", |
34 | "error/errorinterno.html", |
35 | "error/faltaid.html", |
36 | "error/faltanombre.html", |
37 | "error/idincorrecto.html", |
38 | "error/modificacionincorrecta.html", |
39 | "error/nombreenblanco.html", |
40 | "error/nombreincorrecto.html", |
41 | "error/resultadonojson.html", |
42 | "js/esperaUnPocoYSincroniza.js", |
43 | "js/registraServiceWorker.js", |
44 | "js/renderiza.js", |
45 | "js/sincroniza.js", |
46 | "js/bd/Bd.js", |
47 | "js/bd/pasatiempoAgrega.js", |
48 | "js/bd/pasatiempoBusca.js", |
49 | "js/bd/pasatiempoConsultaNoEliminados.js", |
50 | "js/bd/pasatiempoConsultaTodos.js", |
51 | "js/bd/pasatiempoElimina.js", |
52 | "js/bd/pasatiempoModifica.js", |
53 | "js/bd/pasatiemposReemplaza.js", |
54 | "js/modelo/PASATIEMPO.js", |
55 | "js/modelo/validaId.js", |
56 | "js/modelo/validaNombre.js", |
57 | "js/modelo/validaPasatiempo.js", |
58 | "js/modelo/validaPasatiempos.js", |
59 | "lib/js/bdConsulta.js", |
60 | "lib/js/bdEjecuta.js", |
61 | "lib/js/consumeJson.js", |
62 | "lib/js/creaIdCliente.js", |
63 | "lib/js/enviaJson.js", |
64 | "lib/js/exportaAHtml.js", |
65 | "lib/js/htmlentities.js", |
66 | "lib/js/muestraError.js", |
67 | "lib/js/muestraObjeto.js", |
68 | "lib/js/ProblemDetails.js", |
69 | "/" |
70 | ] |
71 | |
72 | // Verifica si el código corre dentro de un service worker. |
73 | if (self instanceof ServiceWorkerGlobalScope) { |
74 | // Evento al empezar a instalar el servide worker, |
75 | self.addEventListener("install", |
76 | (/** @type {ExtendableEvent} */ evt) => { |
77 | console.log("El service worker se está instalando.") |
78 | evt.waitUntil(llenaElCache()) |
79 | }) |
80 | |
81 | // Evento al solicitar información a la red. |
82 | self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => { |
83 | if (evt.request.method === "GET") { |
84 | evt.respondWith(buscaLaRespuestaEnElCache(evt)) |
85 | } |
86 | }) |
87 | |
88 | // Evento cuando el service worker se vuelve activo. |
89 | self.addEventListener("activate", |
90 | () => console.log("El service worker está activo.")) |
91 | } |
92 | |
93 | async function llenaElCache() { |
94 | console.log("Intentando cargar caché:", CACHE) |
95 | // Borra todos los cachés. |
96 | const keys = await caches.keys() |
97 | for (const key of keys) { |
98 | await caches.delete(key) |
99 | } |
100 | // Abre el caché de este service worker. |
101 | const cache = await caches.open(CACHE) |
102 | // Carga el listado de ARCHIVOS. |
103 | await cache.addAll(ARCHIVOS) |
104 | console.log("Cache cargado:", CACHE) |
105 | console.log("Versión:", VERSION) |
106 | } |
107 | |
108 | /** @param {FetchEvent} evt */ |
109 | async function buscaLaRespuestaEnElCache(evt) { |
110 | // Abre el caché. |
111 | const cache = await caches.open(CACHE) |
112 | const request = evt.request |
113 | /* Busca la respuesta a la solicitud en el contenido del caché, sin |
114 | * tomar en cuenta la parte después del símbolo "?" en la URL. */ |
115 | const response = await cache.match(request, { ignoreSearch: true }) |
116 | if (response === undefined) { |
117 | /* Si no la encuentra, empieza a descargar de la red y devuelve |
118 | * la promesa. */ |
119 | return fetch(request) |
120 | } else { |
121 | // Si la encuentra, devuelve la respuesta encontrada en el caché. |
122 | return response |
123 | } |
124 | } |
1 | import { exportaAHtml } from "../lib/js/exportaAHtml.js" |
2 | import { sincroniza } from "./sincroniza.js" |
3 | |
4 | /** |
5 | * Cada 20 segundos (2000 milisegundos) después de la última |
6 | * sincronización, los datos se envían al servidor para volver a |
7 | * sincronizarse con los datos del servidor. |
8 | */ |
9 | const MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR = 20000 |
10 | |
11 | /** |
12 | * @param {HTMLUListElement} lista |
13 | */ |
14 | export function esperaUnPocoYSincroniza(lista) { |
15 | setTimeout(() => sincroniza(lista), MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR) |
16 | } |
17 | |
18 | exportaAHtml(esperaUnPocoYSincroniza) |
1 | "use strict" // usa JavaScript en modo estricto. |
2 | |
3 | const nombreDeServiceWorker = "sw.js" |
4 | |
5 | try { |
6 | navigator.serviceWorker.register(nombreDeServiceWorker) |
7 | .then(registro => { |
8 | console.log(nombreDeServiceWorker, "registrado.") |
9 | console.log(registro) |
10 | }) |
11 | .catch(error => console.log(error)) |
12 | } catch (error) { |
13 | console.log(error) |
14 | } |
1 | import { exportaAHtml } from "../lib/js/exportaAHtml.js" |
2 | import { htmlentities } from "../lib/js/htmlentities.js" |
3 | |
4 | /** |
5 | * @param {HTMLUListElement} lista |
6 | * @param {import("./modelo/PASATIEMPO.js").PASATIEMPO[]} pasatiempos |
7 | */ |
8 | export function renderiza(lista, pasatiempos) { |
9 | let render = "" |
10 | for (const modelo of pasatiempos) { |
11 | if (modelo.PAS_ID === undefined) |
12 | throw new Error(`Falta PAS_ID de ${modelo.PAS_NOMBRE}.`) |
13 | const nombre = htmlentities(modelo.PAS_NOMBRE) |
14 | const searchParams = new URLSearchParams([["id", modelo.PAS_ID]]) |
15 | const params = htmlentities(searchParams.toString()) |
16 | render += /* html */ |
17 | `<li> |
18 | <p><a href="modifica.html?${params}">${nombre}</a></p> |
19 | </li>` |
20 | } |
21 | lista.innerHTML = render |
22 | } |
23 | |
24 | exportaAHtml(renderiza) |
1 | import { enviaJson } from "../lib/js/enviaJson.js" |
2 | import { exportaAHtml } from "../lib/js/exportaAHtml.js" |
3 | import { muestraError } from "../lib/js/muestraError.js" |
4 | import { pasatiempoConsultaTodos } from "./bd/pasatiempoConsultaTodos.js" |
5 | import { pasatiemposReemplaza } from "./bd/pasatiemposReemplaza.js" |
6 | import { esperaUnPocoYSincroniza } from "./esperaUnPocoYSincroniza.js" |
7 | import { validaPasatiempos } from "./modelo/validaPasatiempos.js" |
8 | import { renderiza } from "./renderiza.js" |
9 | |
10 | /** |
11 | * @param {HTMLUListElement} lista |
12 | */ |
13 | export async function sincroniza(lista) { |
14 | try { |
15 | if (navigator.onLine) { |
16 | const todos = await pasatiempoConsultaTodos() |
17 | const respuesta = await enviaJson("srv/sincroniza.php", todos) |
18 | const pasatiempos = validaPasatiempos(respuesta.body) |
19 | await pasatiemposReemplaza(pasatiempos) |
20 | renderiza(lista, pasatiempos) |
21 | } |
22 | } catch (error) { |
23 | muestraError(error) |
24 | } |
25 | esperaUnPocoYSincroniza(lista) |
26 | |
27 | } |
28 | |
29 | exportaAHtml(sincroniza) |
1 | export const ALMACEN_PASATIEMPO = "PASATIEMPO" |
2 | export const PAS_ID = "PAS_ID" |
3 | export const INDICE_NOMBRE = "INDICE_NOMBRE" |
4 | export const PAS_NOMBRE = "PAS_NOMBRE" |
5 | const BD_NOMBRE = "sincronizacion" |
6 | const BD_VERSION = 1 |
7 | |
8 | /** @type { Promise<IDBDatabase> } */ |
9 | export const Bd = new Promise((resolve, reject) => { |
10 | |
11 | /* Se solicita abrir la base de datos, indicando nombre y |
12 | * número de versión. */ |
13 | const solicitud = indexedDB.open(BD_NOMBRE, BD_VERSION) |
14 | |
15 | // Si se presenta un error, rechaza la promesa. |
16 | solicitud.onerror = () => reject(solicitud.error) |
17 | |
18 | // Si se abre con éxito, devuelve una conexión a la base de datos. |
19 | solicitud.onsuccess = () => resolve(solicitud.result) |
20 | |
21 | // Si es necesario, se inicia una transacción para cambio de versión. |
22 | solicitud.onupgradeneeded = () => { |
23 | |
24 | const bd = solicitud.result |
25 | |
26 | // Como hay cambio de versión, borra el almacén si es que existe. |
27 | if (bd.objectStoreNames.contains(ALMACEN_PASATIEMPO)) { |
28 | bd.deleteObjectStore(ALMACEN_PASATIEMPO) |
29 | } |
30 | |
31 | // Crea el almacén "PASATIEMPO" con el campo llave "PAS_ID". |
32 | const almacenPasatiempo = |
33 | bd.createObjectStore(ALMACEN_PASATIEMPO, { keyPath: PAS_ID }) |
34 | |
35 | // Crea un índice ordenado por el campo "PAS_NOMBRE" que no acepta duplicados. |
36 | almacenPasatiempo.createIndex(INDICE_NOMBRE, "PAS_NOMBRE") |
37 | } |
38 | |
39 | }) |
1 | import { bdEjecuta } from "../../lib/js/bdEjecuta.js" |
2 | import { creaIdCliente } from "../../lib/js/creaIdCliente.js" |
3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
4 | import { validaNombre } from "../modelo/validaNombre.js" |
5 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
6 | |
7 | /** |
8 | * @param {import("../modelo/PASATIEMPO.js").PASATIEMPO} modelo |
9 | */ |
10 | export async function pasatiempoAgrega(modelo) { |
11 | validaNombre(modelo.PAS_NOMBRE) |
12 | modelo.PAS_MODIFICACION = Date.now() |
13 | modelo.PAS_ELIMINADO = 0 |
14 | // Genera id único en internet. |
15 | modelo.PAS_ID = creaIdCliente(Date.now().toString()) |
16 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
17 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
18 | almacenPasatiempo.add(modelo) |
19 | }) |
20 | } |
21 | |
22 | exportaAHtml(pasatiempoAgrega) |
1 | import { bdConsulta } from "../../lib/js/bdConsulta.js" |
2 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
3 | import { validaPasatiempo } from "../modelo/validaPasatiempo.js" |
4 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
5 | |
6 | /** |
7 | * @param {string} id |
8 | */ |
9 | export async function pasatiempoBusca(id) { |
10 | |
11 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
12 | /** |
13 | * @param {(resultado: import("../modelo/PASATIEMPO.js").PASATIEMPO|undefined) |
14 | * => any} resolve |
15 | */ |
16 | (transaccion, resolve) => { |
17 | |
18 | /* Pide el primer objeto de ALMACEN_PASATIEMPO que tenga como llave |
19 | * primaria el valor del parámetro id. */ |
20 | const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).get(id) |
21 | |
22 | // onsuccess se invoca solo una vez, devolviendo el objeto solicitado. |
23 | consulta.onsuccess = () => { |
24 | /* Se recupera el objeto solicitado usando |
25 | * consulta.result |
26 | * Si el objeto no se encuentra se recupera undefined. */ |
27 | const objeto = consulta.result |
28 | if (objeto !== undefined) { |
29 | const modelo = validaPasatiempo(objeto) |
30 | if (modelo.PAS_ELIMINADO === 0) { |
31 | resolve(modelo) |
32 | return |
33 | } |
34 | } |
35 | resolve(undefined) |
36 | |
37 | } |
38 | |
39 | }) |
40 | |
41 | } |
42 | |
43 | exportaAHtml(pasatiempoBusca) |
1 | import { bdConsulta } from "../../lib/js/bdConsulta.js" |
2 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
3 | import { validaPasatiempo } from "../modelo/validaPasatiempo.js" |
4 | import { ALMACEN_PASATIEMPO, Bd, INDICE_NOMBRE } from "./Bd.js" |
5 | |
6 | export async function pasatiempoConsultaNoEliminados() { |
7 | |
8 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
9 | /** |
10 | * @param {(resultado: import("../modelo/PASATIEMPO.js").PASATIEMPO[])=>void |
11 | * } resolve |
12 | */ |
13 | (transaccion, resolve) => { |
14 | |
15 | const resultado = [] |
16 | |
17 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
18 | |
19 | // Usa el índice INDICE_NOMBRE para recuperar los datos ordenados. |
20 | const indiceNombre = almacenPasatiempo.index(INDICE_NOMBRE) |
21 | |
22 | // Pide un cursor para recorrer cada objeto que devuelve la consulta. |
23 | const consulta = indiceNombre.openCursor() |
24 | |
25 | /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez |
26 | * cuando se acaban dichos objetos. */ |
27 | consulta.onsuccess = () => { |
28 | /* El cursor correspondiente al objeto se recupera usando |
29 | * consulta.result */ |
30 | const cursor = consulta.result |
31 | if (cursor === null) { |
32 | /* Si el cursor vale null, ya no hay más objetos que procesar; por lo |
33 | * mismo, se devuelve el resultado con los pasatiempos recuperados, usando |
34 | * resolve(resultado). */ |
35 | resolve(resultado) |
36 | } else { |
37 | /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con |
38 | * cursor.value */ |
39 | const modelo = validaPasatiempo(cursor.value) |
40 | if (modelo.PAS_ELIMINADO === 0) { |
41 | resultado.push(modelo) |
42 | } |
43 | /* Busca el siguiente objeto de la consulta, que se recupera la siguiente |
44 | * vez que se invoque la función onsuccess. */ |
45 | cursor.continue() |
46 | } |
47 | } |
48 | |
49 | }) |
50 | |
51 | } |
52 | |
53 | exportaAHtml(pasatiempoConsultaNoEliminados) |
1 | import { bdConsulta } from "../../lib/js/bdConsulta.js" |
2 | import { validaPasatiempo } from "../modelo/validaPasatiempo.js" |
3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
4 | |
5 | /** |
6 | * Lista todos los objetos, incluyendo los que tienen borrado lógico. |
7 | */ |
8 | export async function pasatiempoConsultaTodos() { |
9 | |
10 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
11 | /** |
12 | * @param {(resultado: import("../modelo/PASATIEMPO.js").PASATIEMPO[])=>void |
13 | * } resolve |
14 | */ |
15 | (transaccion, resolve) => { |
16 | |
17 | const resultado = [] |
18 | |
19 | // Pide un cursor para recorrer cada objeto que devuelve la consulta. |
20 | const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).openCursor() |
21 | |
22 | /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez |
23 | * cuando se acaban dichos objetos. */ |
24 | consulta.onsuccess = () => { |
25 | /* El cursor correspondiente al objeto se recupera usando |
26 | * consulta.result */ |
27 | const cursor = consulta.result |
28 | if (cursor === null) { |
29 | /* Si el cursor vale null, ya no hay más objetos que procesar; por lo |
30 | * mismo, se devuelve el resultado con los pasatiempos recuperados, usando |
31 | * resolve(resultado). */ |
32 | resolve(resultado) |
33 | } else { |
34 | /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con |
35 | * cursor.value*/ |
36 | resultado.push(validaPasatiempo(cursor.value)) |
37 | /* Busca el siguiente objeto de la consulta, que se recupera la siguiente |
38 | * vez que se invoque la función onsuccess. */ |
39 | cursor.continue() |
40 | } |
41 | } |
42 | |
43 | }) |
44 | |
45 | } |
1 | import { bdEjecuta } from "../../lib/js/bdEjecuta.js" |
2 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
4 | import { pasatiempoBusca } from "./pasatiempoBusca.js" |
5 | |
6 | /** |
7 | * @param { string } id |
8 | */ |
9 | export async function pasatiempoElimina(id) { |
10 | const modelo = await pasatiempoBusca(id) |
11 | if (modelo !== undefined) { |
12 | modelo.PAS_MODIFICACION = Date.now() |
13 | modelo.PAS_ELIMINADO = 1 |
14 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
15 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
16 | almacenPasatiempo.put(modelo) |
17 | }) |
18 | } |
19 | } |
20 | |
21 | exportaAHtml(pasatiempoElimina) |
1 | import { bdEjecuta } from "../../lib/js/bdEjecuta.js" |
2 | import { exportaAHtml } from "../../lib/js/exportaAHtml.js" |
3 | import { validaId } from "../modelo/validaId.js" |
4 | import { validaNombre } from "../modelo/validaNombre.js" |
5 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
6 | import { pasatiempoBusca } from "./pasatiempoBusca.js" |
7 | |
8 | /** |
9 | * @param { import("../modelo/PASATIEMPO.js").PASATIEMPO } modelo |
10 | */ |
11 | export async function pasatiempoModifica(modelo) { |
12 | validaNombre(modelo.PAS_NOMBRE) |
13 | if (modelo.PAS_ID === undefined) |
14 | throw new Error(`Falta PAS_ID de ${modelo.PAS_NOMBRE}.`) |
15 | validaId(modelo.PAS_ID) |
16 | const anterior = await pasatiempoBusca(modelo.PAS_ID) |
17 | if (anterior !== undefined) { |
18 | modelo.PAS_MODIFICACION = Date.now() |
19 | modelo.PAS_ELIMINADO = 0 |
20 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
21 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
22 | almacenPasatiempo.put(modelo) |
23 | }) |
24 | } |
25 | } |
26 | |
27 | exportaAHtml(pasatiempoModifica) |
1 | import { bdEjecuta } from "../../lib/js/bdEjecuta.js" |
2 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
3 | |
4 | /** |
5 | * Borra el contenido del almacén PASATIEMPO y guarda nuevospasatiempos. |
6 | * @param {import("../modelo/PASATIEMPO.js").PASATIEMPO[]} nuevospasatiempos |
7 | */ |
8 | export async function pasatiemposReemplaza(nuevospasatiempos) { |
9 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
10 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
11 | almacenPasatiempo.clear() |
12 | for (const objeto of nuevospasatiempos) { |
13 | almacenPasatiempo.add(objeto) |
14 | } |
15 | }) |
16 | } |
1 | /** |
2 | * @typedef {Object} PASATIEMPO |
3 | * @property {string} [PAS_ID] |
4 | * @property {string} PAS_NOMBRE |
5 | * @property {number} [PAS_MODIFICACION] |
6 | * @property {number} [PAS_ELIMINADO] |
7 | */ |
1 | /** |
2 | * @param {string} id |
3 | */ |
4 | export function validaId(id) { |
5 | if (id === "") |
6 | throw new Error("Falta el id.") |
7 | } |
1 | /** |
2 | * @param {string} nombre |
3 | */ |
4 | export function validaNombre(nombre) { |
5 | if (nombre === "") |
6 | throw new Error("Falta el nombre.") |
7 | } |
1 | /** |
2 | * @param { any } objeto |
3 | * @returns {import("./PASATIEMPO.js").PASATIEMPO} |
4 | */ |
5 | export function validaPasatiempo(objeto) { |
6 | |
7 | if (typeof objeto.PAS_ID !== "string") |
8 | throw new Error("El id debe ser texto.") |
9 | |
10 | if (typeof objeto.PAS_NOMBRE !== "string") |
11 | throw new Error("El nombre debe ser texto.") |
12 | |
13 | if (typeof objeto.PAS_MODIFICACION !== "number") |
14 | throw new Error("El campo modificacion debe ser número.") |
15 | |
16 | if (typeof objeto.PAS_ELIMINADO !== "number") |
17 | throw new Error("El campo eliminado debe ser número.") |
18 | |
19 | return objeto |
20 | |
21 | } |
1 | import { validaPasatiempo } from "./validaPasatiempo.js" |
2 | |
3 | /** |
4 | * @param { any } objetos |
5 | * @returns {import("./PASATIEMPO.js").PASATIEMPO[]} |
6 | */ |
7 | export function validaPasatiempos(objetos) { |
8 | if (!Array.isArray(objetos)) |
9 | throw new Error("no se recibió un arreglo.") |
10 | /** |
11 | * @type {import("./PASATIEMPO.js").PASATIEMPO[]} |
12 | */ |
13 | const arreglo = [] |
14 | for (const objeto of objetos) { |
15 | arreglo.push(validaPasatiempo(objeto)) |
16 | } |
17 | return arreglo |
18 | } |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/recuperaJson.php"; |
5 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
6 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
7 | require_once __DIR__ . "/../lib/php/devuelveProblemDetails.php"; |
8 | require_once __DIR__ . "/../lib/php/devuelveErrorInterno.php"; |
9 | require_once __DIR__ . "/modelo/TABLA_PASATIEMPO.php"; |
10 | require_once __DIR__ . "/modelo/validaPasatiempo.php"; |
11 | require_once __DIR__ . "/bd/pasatiempoAgrega.php"; |
12 | require_once __DIR__ . "/bd/pasatiempoBusca.php"; |
13 | require_once __DIR__ . "/bd/pasatiempoConsultaNoEliminados.php"; |
14 | require_once __DIR__ . "/bd/pasatiempoModifica.php"; |
15 | |
16 | ejecutaServicio(function () { |
17 | |
18 | $lista = recuperaJson(); |
19 | |
20 | if (!is_array($lista)) { |
21 | $lista = []; |
22 | } |
23 | |
24 | foreach ($lista as $modelo) { |
25 | $modeloEnElCliente = validaPasatiempo($modelo); |
26 | $modeloEnElServidor = pasatiempoBusca($modeloEnElCliente[PAS_ID]); |
27 | |
28 | if ($modeloEnElServidor === false) { |
29 | |
30 | /* CONFLICTO: El modelo no ha estado en el servidor. |
31 | * AGREGARLO solamente si no está eliminado. */ |
32 | if ($modeloEnElCliente[PAS_ELIMINADO] === 0) { |
33 | pasatiempoAgrega($modeloEnElCliente); |
34 | } |
35 | } elseif ( |
36 | $modeloEnElServidor[PAS_ELIMINADO] === 0 |
37 | && $modeloEnElCliente[PAS_ELIMINADO] === 1 |
38 | ) { |
39 | |
40 | /* CONFLICTO: El registro está en el servidor, donde no se ha eliminado, pero |
41 | * ha sido eliminado en el cliente. |
42 | * Gana el cliente, porque optamos por no revivir lo eliminado. */ |
43 | pasatiempoModifica($modeloEnElCliente); |
44 | } else if ( |
45 | $modeloEnElCliente[PAS_ELIMINADO] === 0 |
46 | && $modeloEnElServidor[PAS_ELIMINADO] === 0 |
47 | ) { |
48 | |
49 | /* CONFLICTO: Registros en el servidor y en el cliente. Pueden ser |
50 | * diferentes. |
51 | * GANA FECHA MÁS GRANDE. Cuando gana el servidor, no se hace nada. */ |
52 | if ( |
53 | $modeloEnElCliente[PAS_MODIFICACION] > |
54 | $modeloEnElServidor[PAS_MODIFICACION] |
55 | ) { |
56 | // La versión del cliente es más nueva y prevalece. |
57 | pasatiempoModifica($modeloEnElCliente); |
58 | } |
59 | } |
60 | } |
61 | |
62 | $lista = pasatiempoConsultaNoEliminados(); |
63 | |
64 | devuelveJson($lista); |
65 | }); |
66 |
1 | <?php |
2 | |
3 | class Bd |
4 | { |
5 | |
6 | private static ?PDO $pdo = null; |
7 | |
8 | static function pdo(): PDO |
9 | { |
10 | if (self::$pdo === null) { |
11 | self::$pdo = new PDO( |
12 | // cadena de conexión |
13 | "sqlite:sincronizacion.db", |
14 | // usuario |
15 | null, |
16 | // contraseña |
17 | null, |
18 | // Opciones: pdos no persistentes y lanza excepciones. |
19 | [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] |
20 | ); |
21 | |
22 | self::$pdo->exec( |
23 | 'CREATE TABLE IF NOT EXISTS PASATIEMPO ( |
24 | PAS_ID TEXT NOT NULL, |
25 | PAS_NOMBRE TEXT NOT NULL, |
26 | PAS_MODIFICACION INTEGER NOT NULL, |
27 | PAS_ELIMINADO INTEGER NOT NULL, |
28 | CONSTRAINT PAS_PK |
29 | PRIMARY KEY(PAS_ID), |
30 | CONSTRAINT PAS_ID_NV |
31 | CHECK(LENGTH(PAS_ID) > 0), |
32 | CONSTRAINT PAS_NOM_NV |
33 | CHECK(LENGTH(PAS_NOMBRE) > 0) |
34 | )' |
35 | ); |
36 | } |
37 | |
38 | return self::$pdo; |
39 | } |
40 | } |
41 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/validaNombre.php"; |
4 | require_once __DIR__ . "/../../lib/php/insert.php"; |
5 | require_once __DIR__ . "/Bd.php"; |
6 | require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php"; |
7 | require_once __DIR__ . "/../modelo/validaId.php"; |
8 | |
9 | /** |
10 | * @param array{ |
11 | * PAS_ID: string, |
12 | * PAS_NOMBRE: string, |
13 | * PAS_MODIFICACION: int, |
14 | * PAS_ELIMINADO: int |
15 | * } $modelo |
16 | */ |
17 | function pasatiempoAgrega(array $modelo) |
18 | { |
19 | validaId($modelo[PAS_ID]); |
20 | validaNombre($modelo[PAS_NOMBRE]); |
21 | insert(pdo: Bd::pdo(), into: PASATIEMPO, values: $modelo); |
22 | } |
23 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/selectFirst.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php"; |
6 | |
7 | /** |
8 | * @return false | array{ |
9 | * PAS_ID: string, |
10 | * PAS_NOMBRE: string, |
11 | * PAS_MODIFICACION: int, |
12 | * PAS_ELIMINADO: int |
13 | * } |
14 | */ |
15 | function pasatiempoBusca(string $id): false|array |
16 | { |
17 | return selectFirst( |
18 | pdo: Bd::pdo(), |
19 | from: PASATIEMPO, |
20 | where: [PAS_ID => $id] |
21 | ); |
22 | } |
23 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/select.php"; |
4 | require_once __DIR__ . "/Bd.php"; |
5 | require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php"; |
6 | |
7 | /** |
8 | * @return array{ |
9 | * PAS_ID: string, |
10 | * PAS_NOMBRE: string, |
11 | * PAS_MODIFICACION: int, |
12 | * PAS_ELIMINADO: int |
13 | * }[] |
14 | */ |
15 | function pasatiempoConsultaNoEliminados() |
16 | { |
17 | return select( |
18 | pdo: Bd::pdo(), |
19 | from: PASATIEMPO, |
20 | where: [PAS_ELIMINADO => 0], |
21 | orderBy: PAS_NOMBRE |
22 | ); |
23 | } |
24 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/validaNombre.php"; |
4 | require_once __DIR__ . "/../../lib/php/update.php"; |
5 | require_once __DIR__ . "/Bd.php"; |
6 | require_once __DIR__ . "/../modelo/TABLA_PASATIEMPO.php"; |
7 | require_once __DIR__ . "/../modelo/validaId.php"; |
8 | |
9 | /** |
10 | * @param array{ |
11 | * PAS_ID: string, |
12 | * PAS_NOMBRE: string, |
13 | * PAS_MODIFICACION: int, |
14 | * PAS_ELIMINADO: int |
15 | * } $modelo |
16 | */ |
17 | function pasatiempoModifica(array $modelo) |
18 | { |
19 | validaId($modelo[PAS_ID]); |
20 | validaNombre($modelo[PAS_NOMBRE]); |
21 | update( |
22 | pdo: Bd::pdo(), |
23 | table: PASATIEMPO, |
24 | set: $modelo, |
25 | where: [PAS_ID => $modelo[PAS_ID]] |
26 | ); |
27 | } |
28 |
1 | <?php |
2 | |
3 | const PASATIEMPO = "PASATIEMPO"; |
4 | const PAS_ID = "PAS_ID"; |
5 | const PAS_NOMBRE = "PAS_NOMBRE"; |
6 | const PAS_MODIFICACION = "PAS_MODIFICACION"; |
7 | const PAS_ELIMINADO = "PAS_ELIMINADO"; |
8 |
1 | <?php |
2 | |
3 | function validaId(string $id) |
4 | { |
5 | if ($id === "") |
6 | throw new ProblemDetails( |
7 | status: BAD_REQUEST, |
8 | title: "Falta el id.", |
9 | type: "/error/faltaid.html", |
10 | ); |
11 | } |
12 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../../lib/php/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/../../lib/php/validaJson.php"; |
5 | require_once __DIR__ . "/../../lib/php/ProblemDetails.php"; |
6 | require_once __DIR__ . "/TABLA_PASATIEMPO.php"; |
7 | |
8 | function validaPasatiempo($objeto) |
9 | { |
10 | |
11 | $objeto = validaJson($objeto); |
12 | |
13 | if (!isset($objeto->PAS_ID) || !is_string($objeto->PAS_ID)) |
14 | throw new ProblemDetails( |
15 | status: BAD_REQUEST, |
16 | title: "El id debe ser texto.", |
17 | type: "/error/idincorrecto.html", |
18 | ); |
19 | |
20 | if (!isset($objeto->PAS_NOMBRE) || !is_string($objeto->PAS_NOMBRE)) |
21 | throw new ProblemDetails( |
22 | status: BAD_REQUEST, |
23 | title: "El nombre debe ser texto.", |
24 | type: "/error/nombreincorrecto.html", |
25 | ); |
26 | |
27 | if (!isset($objeto->PAS_MODIFICACION) || !is_int($objeto->PAS_MODIFICACION)) |
28 | throw new ProblemDetails( |
29 | status: BAD_REQUEST, |
30 | title: "La modificacion debe ser número.", |
31 | type: "/error/modificacionincorrecta.html", |
32 | ); |
33 | |
34 | if (!isset($objeto->PAS_ELIMINADO) || !is_int($objeto->PAS_ELIMINADO)) |
35 | throw new ProblemDetails( |
36 | status: BAD_REQUEST, |
37 | title: "El campo eliminado debe ser entero.", |
38 | type: "/error/eliminadoincorrecto.html", |
39 | ); |
40 | |
41 | return [ |
42 | PAS_ID => $objeto->PAS_ID, |
43 | PAS_NOMBRE => $objeto->PAS_NOMBRE, |
44 | PAS_MODIFICACION => $objeto->PAS_MODIFICACION, |
45 | PAS_ELIMINADO => $objeto->PAS_ELIMINADO |
46 | ]; |
47 | } |
48 |
1 | /** |
2 | * @template T |
3 | * @param {Promise<IDBDatabase>} bd |
4 | * @param {string[]} almacenes |
5 | * @param {(transaccion: IDBTransaction, resolve: (resultado:T)=>void) => any |
6 | * } consulta |
7 | * @returns {Promise<T>} |
8 | */ |
9 | export async function bdConsulta(bd, almacenes, consulta) { |
10 | |
11 | const base = await bd |
12 | |
13 | return new Promise((resolve, reject) => { |
14 | // Inicia una transacción de solo lectura. |
15 | const transaccion = base.transaction(almacenes, "readonly") |
16 | // Al terminar con error ejecuta la función reject. |
17 | transaccion.onerror = () => reject(transaccion.error) |
18 | // Estas son las operaciones para realizar la consulta. |
19 | consulta(transaccion, resolve) |
20 | }) |
21 | |
22 | } |
1 | /** |
2 | * @param {Promise<IDBDatabase>} bd |
3 | * @param {string[]} entidades |
4 | * @param {(t:IDBTransaction) => void} operaciones |
5 | */ |
6 | export async function bdEjecuta(bd, entidades, operaciones) { |
7 | |
8 | // Espera que se abra la bd |
9 | const base = await bd |
10 | |
11 | return new Promise( |
12 | (resolve, reject) => { |
13 | // Inicia una transacción de lectura y escritura. |
14 | const transaccion = base.transaction(entidades, "readwrite") |
15 | // Al terminar con éxito, ejecuta la función resolve. |
16 | transaccion.oncomplete = resolve |
17 | // Al terminar con error, ejecuta la función reject. |
18 | transaccion.onerror = () => reject(transaccion.error) |
19 | // Estas son las operaciones de la transacción. |
20 | operaciones(transaccion) |
21 | }) |
22 | |
23 | } |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Espera a que la promesa de un fetch termine. Si |
6 | * hay error, lanza una excepción. Si no hay error, |
7 | * interpreta la respuesta del servidor como JSON y |
8 | * la convierte en una literal de objeto. |
9 | * |
10 | * @param { string | Promise<Response> } servicio |
11 | */ |
12 | export async function consumeJson(servicio) { |
13 | |
14 | if (typeof servicio === "string") { |
15 | servicio = fetch(servicio, { |
16 | headers: { "Accept": "application/json, application/problem+json" } |
17 | }) |
18 | } else if (!(servicio instanceof Promise)) { |
19 | throw new Error("Servicio de tipo incorrecto.") |
20 | } |
21 | |
22 | const respuesta = await servicio |
23 | |
24 | const headers = respuesta.headers |
25 | |
26 | if (respuesta.ok) { |
27 | // Aparentemente el servidor tuvo éxito. |
28 | |
29 | if (respuesta.status === 204) { |
30 | // No contiene texto de respuesta. |
31 | |
32 | return { headers, body: {} } |
33 | |
34 | } else { |
35 | |
36 | const texto = await respuesta.text() |
37 | |
38 | try { |
39 | |
40 | return { headers, body: JSON.parse(texto) } |
41 | |
42 | } catch (error) { |
43 | |
44 | // El contenido no es JSON. Probablemente sea texto de un error. |
45 | throw new ProblemDetails(respuesta.status, headers, texto, |
46 | "/error/errorinterno.html") |
47 | |
48 | } |
49 | |
50 | } |
51 | |
52 | } else { |
53 | // Hay un error. |
54 | |
55 | const texto = await respuesta.text() |
56 | |
57 | if (texto === "") { |
58 | |
59 | // No hay texto. Se usa el texto predeterminado. |
60 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText) |
61 | |
62 | } else { |
63 | // Debiera se un ProblemDetails en JSON. |
64 | |
65 | try { |
66 | |
67 | const { title, type, detail } = JSON.parse(texto) |
68 | |
69 | throw new ProblemDetails(respuesta.status, headers, |
70 | typeof title === "string" ? title : respuesta.statusText, |
71 | typeof type === "string" ? type : undefined, |
72 | typeof detail === "string" ? detail : undefined) |
73 | |
74 | } catch (error) { |
75 | |
76 | if (error instanceof ProblemDetails) { |
77 | // El error si era un ProblemDetails |
78 | |
79 | throw error |
80 | |
81 | } else { |
82 | |
83 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText, |
84 | undefined, texto) |
85 | |
86 | } |
87 | |
88 | } |
89 | |
90 | } |
91 | |
92 | } |
93 | |
94 | } |
95 | |
96 | exportaAHtml(consumeJson) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | |
3 | /** |
4 | * Añade caracteres al azar a una raíz, para obtener un clientId único. |
5 | * @param {string} raiz |
6 | */ |
7 | export function creaIdCliente(raiz) { |
8 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
9 | for (var i = 0; i < 15; i++) { |
10 | raiz += chars.charAt(Math.floor(Math.random() * chars.length)) |
11 | } |
12 | return raiz |
13 | } |
14 | |
15 | exportaAHtml(creaIdCliente) |
1 | import { consumeJson } from "./consumeJson.js" |
2 | import { exportaAHtml } from "./exportaAHtml.js" |
3 | |
4 | /** |
5 | * @param { string } url |
6 | * @param { Object } body |
7 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
8 | * | "CONNECT" | "HEAD" } metodoHttp |
9 | */ |
10 | export async function enviaJson(url, body, metodoHttp = "POST") { |
11 | return await consumeJson(fetch(url, { |
12 | method: metodoHttp, |
13 | headers: { |
14 | "Content-Type": "application/json", |
15 | "Accept": "application/json, application/problem+json" |
16 | }, |
17 | body: JSON.stringify(body) |
18 | })) |
19 | } |
20 | |
21 | exportaAHtml(enviaJson) |
1 | /** |
2 | * Permite que los eventos de html usen la función. |
3 | * @param {function} functionInstance |
4 | */ |
5 | export function exportaAHtml(functionInstance) { |
6 | window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance |
7 | } |
8 | |
9 | /** |
10 | * @param {function} valor |
11 | */ |
12 | export function nombreDeFuncionParaHtml(valor) { |
13 | const names = valor.name.split(/\s+/g) |
14 | return names[names.length - 1] |
15 | } |
1 | /** |
2 | * Codifica un texto para que cambie los caracteres |
3 | * especiales y no se pueda interpretar como |
4 | * etiiqueta HTML. Esta técnica evita la inyección |
5 | * de código. |
6 | * @param { string } texto |
7 | */ |
8 | export function htmlentities(texto) { |
9 | return texto.replace(/[<>"']/g, textoDetectado => { |
10 | switch (textoDetectado) { |
11 | case "<": return "<" |
12 | case ">": return ">" |
13 | case '"': return """ |
14 | case "'": return "'" |
15 | default: return textoDetectado |
16 | } |
17 | }) |
18 | } |
19 |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Muestra un error en la consola y en un cuadro de |
6 | * alerta el mensaje de una excepción. |
7 | * @param { ProblemDetails | Error | null } error descripción del error. |
8 | */ |
9 | export function muestraError(error) { |
10 | |
11 | if (error === null) { |
12 | |
13 | console.error("Error") |
14 | alert("Error") |
15 | |
16 | } else if (error instanceof ProblemDetails) { |
17 | |
18 | let mensaje = error.title |
19 | if (error.detail) { |
20 | mensaje += `\n\n${error.detail}` |
21 | } |
22 | mensaje += `\n\nCódigo: ${error.status}` |
23 | if (error.type) { |
24 | mensaje += ` ${error.type}` |
25 | } |
26 | |
27 | console.error(mensaje) |
28 | console.error(error) |
29 | console.error("Headers:") |
30 | error.headers.forEach((valor, llave) => console.error(llave, "=", valor)) |
31 | alert(mensaje) |
32 | |
33 | } else { |
34 | |
35 | console.error(error) |
36 | alert(error.message) |
37 | |
38 | } |
39 | |
40 | } |
41 | |
42 | exportaAHtml(muestraError) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | |
3 | /** |
4 | * @param { Document | HTMLElement } raizHtml |
5 | * @param { any } objeto |
6 | */ |
7 | export function muestraObjeto(raizHtml, objeto) { |
8 | |
9 | for (const [nombre, definiciones] of Object.entries(objeto)) { |
10 | |
11 | if (Array.isArray(definiciones)) { |
12 | |
13 | muestraArray(raizHtml, nombre, definiciones) |
14 | |
15 | } else if (definiciones !== undefined && definiciones !== null) { |
16 | |
17 | const elementoHtml = buscaElementoHtml(raizHtml, nombre) |
18 | |
19 | if (elementoHtml instanceof HTMLInputElement) { |
20 | |
21 | muestraInput(raizHtml, elementoHtml, definiciones) |
22 | |
23 | } else if (elementoHtml !== null) { |
24 | |
25 | for (const [atributo, valor] of Object.entries(definiciones)) { |
26 | if (atributo in elementoHtml) { |
27 | elementoHtml[atributo] = valor |
28 | } |
29 | } |
30 | |
31 | } |
32 | |
33 | } |
34 | |
35 | } |
36 | |
37 | } |
38 | exportaAHtml(muestraObjeto) |
39 | |
40 | /** |
41 | * @param { Document | HTMLElement } raizHtml |
42 | * @param { string } nombre |
43 | */ |
44 | export function buscaElementoHtml(raizHtml, nombre) { |
45 | return raizHtml.querySelector( |
46 | `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`) |
47 | } |
48 | |
49 | /** |
50 | * @param { Document | HTMLElement } raizHtml |
51 | * @param { string } propiedad |
52 | * @param {any[]} valores |
53 | */ |
54 | function muestraArray(raizHtml, propiedad, valores) { |
55 | |
56 | const conjunto = new Set(valores) |
57 | const elementos = |
58 | raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`) |
59 | |
60 | if (elementos.length === 1) { |
61 | const elemento = elementos[0] |
62 | |
63 | if (elemento instanceof HTMLSelectElement) { |
64 | const options = elemento.options |
65 | for (let i = 0, len = options.length; i < len; i++) { |
66 | const option = options[i] |
67 | option.selected = conjunto.has(option.value) |
68 | } |
69 | return |
70 | } |
71 | |
72 | } |
73 | |
74 | for (let i = 0, len = elementos.length; i < len; i++) { |
75 | const elemento = elementos[i] |
76 | if (elemento instanceof HTMLInputElement) { |
77 | elemento.checked = conjunto.has(elemento.value) |
78 | } |
79 | } |
80 | |
81 | } |
82 | |
83 | /** |
84 | * @param { Document | HTMLElement } raizHtml |
85 | * @param { HTMLInputElement } input |
86 | * @param { any } definiciones |
87 | */ |
88 | function muestraInput(raizHtml, input, definiciones) { |
89 | |
90 | for (const [atributo, valor] of Object.entries(definiciones)) { |
91 | |
92 | if (atributo == "data-file") { |
93 | |
94 | const img = getImgParaElementoHtml(raizHtml, input) |
95 | if (img !== null) { |
96 | input.dataset.file = valor |
97 | input.value = "" |
98 | if (valor === "") { |
99 | img.src = "" |
100 | img.hidden = true |
101 | } else { |
102 | img.src = valor |
103 | img.hidden = false |
104 | } |
105 | } |
106 | |
107 | } else if (atributo in input) { |
108 | |
109 | input[atributo] = valor |
110 | |
111 | } |
112 | } |
113 | |
114 | } |
115 | |
116 | /** |
117 | * @param { Document | HTMLElement } raizHtml |
118 | * @param { HTMLElement } elementoHtml |
119 | */ |
120 | export function getImgParaElementoHtml(raizHtml, elementoHtml) { |
121 | const imgId = elementoHtml.getAttribute("data-img") |
122 | if (imgId === null) { |
123 | return null |
124 | } else { |
125 | const input = buscaElementoHtml(raizHtml, imgId) |
126 | if (input instanceof HTMLImageElement) { |
127 | return input |
128 | } else { |
129 | return null |
130 | } |
131 | } |
132 | } |
1 | /** |
2 | * Detalle de los errores devueltos por un servicio. |
3 | */ |
4 | export class ProblemDetails extends Error { |
5 | |
6 | /** |
7 | * @param {number} status |
8 | * @param {Headers} headers |
9 | * @param {string} title |
10 | * @param {string} [type] |
11 | * @param {string} [detail] |
12 | */ |
13 | constructor(status, headers, title, type, detail) { |
14 | super(title) |
15 | /** |
16 | * @readonly |
17 | */ |
18 | this.status = status |
19 | /** |
20 | * @readonly |
21 | */ |
22 | this.headers = headers |
23 | /** |
24 | * @readonly |
25 | */ |
26 | this.type = type |
27 | /** |
28 | * @readonly |
29 | */ |
30 | this.detail = detail |
31 | /** |
32 | * @readonly |
33 | */ |
34 | this.title = title |
35 | } |
36 | |
37 | } |
1 | <?php |
2 | |
3 | const BAD_REQUEST = 400; |
4 |
1 | <?php |
2 | |
3 | function calculaArregloDeParametros(array $arreglo) |
4 | { |
5 | $parametros = []; |
6 | foreach ($arreglo as $llave => $valor) { |
7 | $parametros[":$llave"] = $valor; |
8 | } |
9 | return $parametros; |
10 | } |
11 |
1 | <?php |
2 | |
3 | function calculaSqlDeAsignaciones(string $separador, array $arreglo) |
4 | { |
5 | $primerElemento = true; |
6 | $sqlDeAsignacion = ""; |
7 | foreach ($arreglo as $llave => $valor) { |
8 | $sqlDeAsignacion .= |
9 | ($primerElemento === true ? "" : $separador) . "$llave=:$llave"; |
10 | $primerElemento = false; |
11 | } |
12 | return $sqlDeAsignacion; |
13 | } |
14 |
1 | <?php |
2 | |
3 | function calculaSqlDeCamposDeInsert(array $values) |
4 | { |
5 | $primerCampo = true; |
6 | $sqlDeCampos = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeCampos .= ($primerCampo === true ? "" : ",") . "$nombreDeValue"; |
9 | $primerCampo = false; |
10 | } |
11 | return $sqlDeCampos; |
12 | } |
13 |
1 | <?php |
2 | |
3 | function calculaSqlDeValues(array $values) |
4 | { |
5 | $primerValue = true; |
6 | $sqlDeValues = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeValues .= ($primerValue === true ? "" : ",") . ":$nombreDeValue"; |
9 | $primerValue = false; |
10 | } |
11 | return $sqlDeValues; |
12 | } |
13 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | function delete(PDO $pdo, string $from, array $where) |
7 | { |
8 | $sql = "DELETE FROM $from"; |
9 | |
10 | if (sizeof($where) === 0) { |
11 | $pdo->exec($sql); |
12 | } else { |
13 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
14 | $sql .= " WHERE $sqlDeWhere"; |
15 | |
16 | $statement = $pdo->prepare($sql); |
17 | $parametros = calculaArregloDeParametros($where); |
18 | $statement->execute($parametros); |
19 | } |
20 | } |
21 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
6 | |
7 | function devuelveErrorInterno(Throwable $error) |
8 | { |
9 | devuelveProblemDetails(new ProblemDetails( |
10 | status: INTERNAL_SERVER_ERROR, |
11 | title: $error->getMessage(), |
12 | type: "/error/errorinterno.html" |
13 | )); |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | |
5 | function devuelveJson($resultado) |
6 | { |
7 | |
8 | $json = json_encode($resultado); |
9 | |
10 | if ($json === false) { |
11 | |
12 | devuelveResultadoNoJson(); |
13 | } else { |
14 | |
15 | http_response_code(200); |
16 | header("Content-Type: application/json"); |
17 | echo $json; |
18 | } |
19 | } |
20 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function devuelveProblemDetails(ProblemDetails $details) |
7 | { |
8 | |
9 | $body = ["title" => $details->title]; |
10 | if ($details->type !== null) { |
11 | $body["type"] = $details->type; |
12 | } |
13 | if ($details->detail !== null) { |
14 | $body["detail"] = $details->detail; |
15 | } |
16 | |
17 | $json = json_encode($body); |
18 | |
19 | if ($json === false) { |
20 | |
21 | devuelveResultadoNoJson(); |
22 | } else { |
23 | |
24 | http_response_code($details->status); |
25 | header("Content-Type: application/problem+json"); |
26 | echo $json; |
27 | } |
28 | } |
29 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | |
5 | function devuelveResultadoNoJson() |
6 | { |
7 | |
8 | http_response_code(INTERNAL_SERVER_ERROR); |
9 | header("Content-Type: application/problem+json"); |
10 | echo '{' . |
11 | '"title": "El resultado no puede representarse como JSON."' . |
12 | '"type": "/error/resultadonojson.html"' . |
13 | '}'; |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveErrorInterno.php"; |
6 | |
7 | function ejecutaServicio(callable $codigo) |
8 | { |
9 | try { |
10 | $codigo(); |
11 | } catch (ProblemDetails $details) { |
12 | devuelveProblemDetails($details); |
13 | } catch (Throwable $error) { |
14 | devuelveErrorInterno($error); |
15 | } |
16 | } |
17 |
1 | <?php |
2 | |
3 | function fetch( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ) { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return false; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | if ($opcional === null) { |
20 | return $statement->fetch($mode); |
21 | } else { |
22 | $statement->setFetchMode($mode, $opcional); |
23 | return $statement->fetch(); |
24 | } |
25 | } |
26 | } |
27 |
1 | <?php |
2 | |
3 | function fetchAll( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ): array { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return []; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | $resultado = $opcional === null |
20 | ? $statement->fetchAll($mode) |
21 | : $statement->fetchAll($mode, $opcional); |
22 | |
23 | if ($resultado === false) { |
24 | return []; |
25 | } else { |
26 | return $resultado; |
27 | } |
28 | } |
29 | } |
30 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaSqlDeCamposDeInsert.php"; |
4 | require_once __DIR__ . "/calculaSqlDeValues.php"; |
5 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
6 | |
7 | function insert(PDO $pdo, string $into, array $values) |
8 | { |
9 | $sqlDeCampos = calculaSqlDeCamposDeInsert($values); |
10 | $sqlDeValues = calculaSqlDeValues($values); |
11 | $sql = "INSERT INTO $into ($sqlDeCampos) VALUES ($sqlDeValues)"; |
12 | $parametros = calculaArregloDeParametros($values); |
13 | $pdo->prepare($sql)->execute($parametros); |
14 | } |
15 |
1 | <?php |
2 | |
3 | const INTERNAL_SERVER_ERROR = 500; |
1 | <?php |
2 | |
3 | /** Detalle de los errores devueltos por un servicio. */ |
4 | class ProblemDetails extends Exception |
5 | { |
6 | |
7 | public int $status; |
8 | public string $title; |
9 | public ?string $type; |
10 | public ?string $detail; |
11 | |
12 | public function __construct( |
13 | int $status, |
14 | string $title, |
15 | ?string $type = null, |
16 | ?string $detail = null, |
17 | Throwable $previous = null |
18 | ) { |
19 | parent::__construct($title, $status, $previous); |
20 | $this->status = $status; |
21 | $this->type = $type; |
22 | $this->title = $title; |
23 | $this->detail = $detail; |
24 | } |
25 | } |
26 |
1 | <?php |
2 | |
3 | function recuperaJson() |
4 | { |
5 | return json_decode(file_get_contents("php://input")); |
6 | } |
7 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetchAll.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | function select( |
7 | PDO $pdo, |
8 | string $from, |
9 | array $where = [], |
10 | string $orderBy = "", |
11 | int $mode = PDO::FETCH_ASSOC, |
12 | $opcional = null |
13 | ) { |
14 | $sql = "SELECT * FROM $from"; |
15 | |
16 | if (sizeof($where) > 0) { |
17 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
18 | $sql .= " WHERE $sqlDeWhere"; |
19 | } |
20 | |
21 | if ($orderBy !== "") { |
22 | $sql .= " ORDER BY $orderBy"; |
23 | } |
24 | |
25 | if (sizeof($where) === 0) { |
26 | $statement = $pdo->query($sql); |
27 | return fetchAll($statement, [], $mode, $opcional); |
28 | } else { |
29 | $statement = $pdo->prepare($sql); |
30 | $parametros = calculaArregloDeParametros($where); |
31 | return fetchAll($statement, $parametros, $mode, $opcional); |
32 | } |
33 | } |
34 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetch.php"; |
4 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
5 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
6 | |
7 | function selectFirst( |
8 | PDO $pdo, |
9 | string $from, |
10 | array $where = [], |
11 | string $orderBy = "", |
12 | int $mode = PDO::FETCH_ASSOC, |
13 | $opcional = null |
14 | ) { |
15 | $sql = "SELECT * FROM $from"; |
16 | |
17 | if (sizeof($where) > 0) { |
18 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
19 | $sql .= " WHERE $sqlDeWhere"; |
20 | } |
21 | |
22 | if ($orderBy !== "") { |
23 | $sql .= " ORDER BY $orderBy"; |
24 | } |
25 | |
26 | if (sizeof($where) === 0) { |
27 | $statement = $pdo->query($sql); |
28 | return fetch($statement, [], $mode, $opcional); |
29 | } else { |
30 | $statement = $pdo->prepare($sql); |
31 | $parametros = calculaArregloDeParametros($where); |
32 | return fetch($statement, $parametros, $mode, $opcional); |
33 | } |
34 | } |
35 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | |
7 | function update(PDO $pdo, string $table, array $set, array $where) |
8 | { |
9 | $sqlDeSet = calculaSqlDeAsignaciones(",", $set); |
10 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
11 | $sql = "UPDATE $table SET $sqlDeSet WHERE $sqlDeWhere"; |
12 | |
13 | $parametros = calculaArregloDeParametros($set); |
14 | foreach ($where as $nombreDeWhere => $valorDeWhere) { |
15 | $parametros[":$nombreDeWhere"] = $valorDeWhere; |
16 | } |
17 | $statement = $pdo->prepare($sql); |
18 | $statement->execute($parametros); |
19 | } |
20 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function validaJson($objeto) |
7 | { |
8 | |
9 | if ($objeto === null) |
10 | throw new ProblemDetails( |
11 | status: BAD_REQUEST, |
12 | title: "Los datos recibidos no son JSON.", |
13 | type: "/error/datosnojson.html", |
14 | detail: "Los datos recibidos no están en formato JSON.O", |
15 | ); |
16 | |
17 | return $objeto; |
18 | } |
19 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function validaNombre(false|string $nombre) |
7 | { |
8 | |
9 | if ($nombre === false) |
10 | throw new ProblemDetails( |
11 | status: BAD_REQUEST, |
12 | title: "Falta el nombre.", |
13 | type: "/error/faltanombre.html", |
14 | detail: "La solicitud no tiene el valor de nombre." |
15 | ); |
16 | |
17 | $trimNombre = trim($nombre); |
18 | |
19 | if ($trimNombre === "") |
20 | throw new ProblemDetails( |
21 | status: BAD_REQUEST, |
22 | title: "Nombre en blanco.", |
23 | type: "/error/nombreenblanco.html", |
24 | detail: "Pon texto en el campo nombre.", |
25 | ); |
26 | |
27 | return $trimNombre; |
28 | } |
29 |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Los datos recibidos no son JSON</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Los datos recibidos no son JSON</h1> |
16 | |
17 | <p> |
18 | Los datos recibidos no están en formato JSON. |
19 | </p> |
20 | |
21 | </body> |
22 | |
23 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>El campo eliminado debe ser entero</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>El campo eliminado debe ser entero</h1> |
14 | |
15 | </body> |
16 | |
17 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Error interno del servidor</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Error interno del servidor</h1> |
16 | |
17 | <p>Se presentó de forma inesperada un error interno del servidor.</p> |
18 | |
19 | </body> |
20 | |
21 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Falta el id</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Falta el id</h1> |
16 | |
17 | </body> |
18 | |
19 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Falta el nombre</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Falta el nombre</h1> |
16 | |
17 | <p>La solicitud no tiene el valor de nombre.</p> |
18 | |
19 | </body> |
20 | |
21 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>El id debe ser texto</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>El id debe ser texto</h1> |
14 | |
15 | </body> |
16 | |
17 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>La modificacion debe ser número</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>La modificacion debe ser número</h1> |
14 | |
15 | </body> |
16 | |
17 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>Nombre en blanco</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Nombre en blanco</h1> |
16 | |
17 | <p>Pon texto en el campo nombre.</p> |
18 | |
19 | </body> |
20 | |
21 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>El nombre debe ser texto</title> |
10 | |
11 | <body> |
12 | |
13 | <h1>El nombre debe ser texto</h1> |
14 | |
15 | </body> |
16 | |
17 | </html> |
1 | <!DOCTYPE html> |
2 | <html lang="es"> |
3 | |
4 | <head> |
5 | |
6 | <meta charset="UTF-8"> |
7 | <meta name="viewport" content="width=device-width"> |
8 | |
9 | <title>El resultado no puede representarse como JSON</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>El resultado no puede representarse como JSON</h1> |
16 | |
17 | <p> |
18 | Debido a un error interno del servidor, el resultado generado, no se puede |
19 | recuperar. |
20 | </p> |
21 | |
22 | </body> |
23 | |
24 | </html> |
1 | AddType application/manifest+json .webmanifest |
2 | |
3 | ExpiresActive On |
4 | |
5 | Header set Cache-Control "max-age=1, must-revalidate" |
6 | |
7 | RewriteEngine On |
8 | |
9 | RewriteCond %{HTTP:X-Forwarded-Proto} !https |
10 | RewriteCond %{HTTPS} off |
11 | RewriteCond %{HTTP:CF-Visitor} !{"scheme":"https"} |
12 | RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] |
13 |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
1 | { |
2 | "compilerOptions": { |
3 | "checkJs": true, |
4 | "strictNullChecks": true, |
5 | "target": "ES6", |
6 | "module": "Node16", |
7 | "moduleResolution": "Node16", |
8 | "lib": [ |
9 | "ES2017", |
10 | "WebWorker", |
11 | "DOM" |
12 | ] |
13 | } |
14 | } |
En esta lección se muestra un ejemplo de sincronización de bases de datos.