Desarrollo multiplataforma

Versión para imprimir.

por Gilberto Pacheco Gallegos

1. Conocimientos requeridos

2. Instrucciones de navegación

  • 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.

    skip_previous
    arrow_back (Tecla flecha a la izquierda)
    Swipe hacia la derecha

    Regresa a la página anterior.

    skip_next
    arrow_forward (Tecla flecha a la derecha)
    Swipe hacia la izquierda

    Avanza a la página siguiente.

3. Introducción a las PWA

Versión para imprimir.

A. Introduccion

  • En esta lección se introduce el concepto de PWA.

B. 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

C. Requerimientos de instalación y uso

D. Navegadores compatibles

  • 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).

E. Herramientas de desarrollo

  • 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/.

F. Resumen

  • 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.

4. PWA básica

Versión para imprimir.

A. Introduccion

  • En esta lección se presenta una PWA básica.

  • La puedes probar e instalar, de preferencia con Chrome, en https://gilpgpwa.github.io/.

B. Hazlo funcionar

  • Prueba e instala, de preferencia con Chrome, el sitio https://gilpgpwa.github.io/.

  • Revisa el proyecto en Replit con la URl https://replit.com/@GilbertoPachec2/pwa?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  • Una vez que se está ejecutando, probablemente te aparezca un mensaje de error. Ciérralo y haz clic en el botón para ejecutar la app en una pestaña nueva.

  1. 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.

  2. Crea los íconos enmascarables con https://maskable.app/ a partir del archivo «icono2048.png».

  3. Usa o crea una cuenta de Google.

  4. Crea una cuenta de Replit usando la cuenta de Google.

  5. Crea un proyecto HTML, CSS, JS en Replit y edita o sube los archivos de este proyecto.

  6. 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.

  7. Cada vez que publiques los cambios en los archivos, debes modificar el valor de VERSION en el archivo sw.js».

  8. Depura el proyecto.

  9. Recuerda usar la combinación de teclas Ctrl+Mayúsculas+r para forzar que se actualice el navegador en caso de que no se vean los cambios.

  10. Crea la cover page o página de spotlight del proyecto.

  11. 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.

  12. 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.

  13. Instala y usa tu PWA en iOS (iPhone y iPad).

C. Archivos

D. index.html

Salida

Ábrelo en otra pestaña.

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>PWA Básica</title>
10
11 <!-- Resumen para los motores de búsqueda. -->
12 <meta name="description" content="Ejemplo de PWA">
13
14 <!-- Color de la barra de navegación de Chrome en
15 dispositivos móviles. -->
16 <meta name="theme-color" content="#cbc693">
17
18 <!-- Ícono para la página web, que normalmente se
19 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 y Edge.
24 Debe ponerse en todas las páginas. -->
25 <link rel="manifest" href="site.webmanifest">
26
27 <link rel="stylesheet" href="css/estilos.css">
28
29 <script type="module" src="js/configura.js"></script>
30
31</head>
32
33<body>
34
35 <h1>PWA Básica</h1>
36
37 <p>Hola mundo.</p>
38
39</body>
40
41</html>

E. Carpeta « css »

A. css / estilos.css

1html {
2 color-scheme: light dark;
3 font-family: sans-serif;
4}

F. Carpeta « js »

A. js / configura.js

1import {
2 registraServiceWorkerSiEsSoportado
3} from "../lib/js/registraServiceWorkerSiEsSoportado.js"
4
5registraServiceWorkerSiEsSoportado("sw.js")

G. site.webmanifest

  • Este archivo sirve para configurar los instaladores de la aplicación.

Explicación de las propiedades

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",
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}

H. instruccionesListadoSw.txt

1Generar el listado de archivos del sw.js desde Visual Studio Code.
21. Abrir una terminal desde el menú
3 Terminal > New Terminal
4
52. Desde la terminal introducir la orden:
6 Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
83. Abrir el archivo generado, que se llama
9 archivos.txt
10 y sobre este, realizar los pasos que siguen:
11
124. Quita del archivo archivos.txt:
13 * el encabezado,
14 * todas las carpetas,
15 * el archivo .vscode/settings.json,
16 * el archivo archivos.txt,
17 * este archivo (instruccionesListadoSw.txt),
18 * el archivo jsconfig.json,
19 * el archivo sw.js,
20 * el archivo de la base de datos, que termina en ".bd" y
21 está en la carpeta srv,
22 * todos los archivos de php y
23 * las líneas en blanco del final
24
255. Cambia los \ por / desde Visual Studio Code con las siguientes
26 combinaciones de teclas:
27
28 Ctrl+H En el diálogo que aparece introduce lo siguiente:
29 Find:\
30 Replace:/
31
32 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
33
346. Coloca las comillas y coma del final de cada línea desde Visual
35 Studio Code con las siguientes combinaciones de teclas:
36
37 Ctrl+H En el diálogo que aparece, selecciona el botón
38 ".*"
39 e introduce lo siguiente:
40 Find:\s*$
41 Replace:",
42
43 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
44
457. Marca la carpeta inicial, presiona la combinación de teclas:
46
47 Shift+Ctrl+L
48
49 borra la selección, teclea " y luego ESC
50
518. Cambia las secuencias de espacios por / con las siguientes
52 combinaciones de teclas:
53
54 Ctrl+H En el diálogo que aparece, selecciona el botón
55 ".*"
56 e introduce lo siguiente:
57 Find:\s+
58 Replace:/
59
60 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
61
629. Cambia las "/ por " con las siguientes combinaciones de teclas:
63
64 Ctrl+H En el diálogo que aparece, quita la selección del botón
65 ".*"
66 e introduce lo siguiente:
67 Find:"/
68 Replace:"
69
70 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
71
7210. Copia el texto al archivo
73 sw.js
74 en el contenido del arreglo llamado ARCHIVOS, pero recuerda
75 mantener el último elemento, que dice:
76 "/"

I. archivos.txt

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/configura.js",
17"lib/js/muestraError.js",
18"lib/js/ProblemDetails.js",
19"lib/js/registraServiceWorkerSiEsSoportado.js",

J. sw.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 */
18const VERSION = "1.00"
19
20/** Nombre del archivo de cache. */
21const CACHE = "ejemploPWA"
22
23/**
24 * Archivos requeridos para que la aplicación funcione fuera de
25 * línea.
26 */
27const 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/configura.js",
44 "lib/js/muestraError.js",
45 "lib/js/ProblemDetails.js",
46 "lib/js/registraServiceWorkerSiEsSoportado.js",
47 "/"
48]
49
50// Verifica si el código corre dentro de un service worker.
51if (self instanceof ServiceWorkerGlobalScope) {
52 // Evento al empezar a instalar el servide worker,
53 self.addEventListener("install",
54 (/** @type {ExtendableEvent} */ evt) => {
55 console.log("El service worker se está instalando.")
56 evt.waitUntil(llenaElCache())
57 })
58
59 // Evento al solicitar información a la red.
60 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
61 if (evt.request.method === "GET") {
62 evt.respondWith(buscaLaRespuestaEnElCache(evt))
63 }
64 })
65
66 // Evento cuando el service worker se vuelve activo.
67 self.addEventListener("activate",
68 () => console.log("El service worker está activo."))
69}
70
71async function llenaElCache() {
72 console.log("Intentando cargar caché:", CACHE, ".")
73 // Borra todos los cachés.
74 const keys = await caches.keys()
75 for (const key of keys) {
76 await caches.delete(key)
77 }
78 // Abre el caché de este service worker.
79 const cache = await caches.open(CACHE)
80 // Carga el listado de ARCHIVOS.
81 await cache.addAll(ARCHIVOS)
82 console.log("Cache cargado:", CACHE, ".")
83 console.log("Versión:", VERSION, ".")
84}
85
86/** @param {FetchEvent} evt */
87async function buscaLaRespuestaEnElCache(evt) {
88 // Abre el caché.
89 const cache = await caches.open(CACHE)
90 const request = evt.request
91 /* Busca la respuesta a la solicitud en el contenido del caché, sin
92 * tomar en cuenta la parte después del símbolo "?" en la URL. */
93 const response = await cache.match(request, { ignoreSearch: true })
94 if (response === undefined) {
95 /* Si no la encuentra, empieza a descargar de la red y devuelve
96 * la promesa. */
97 return fetch(request)
98 } else {
99 // Si la encuentra, devuelve la respuesta encontrada en el caché.
100 return response
101 }
102}

K. favicon.ico

  • 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.

favicon.ico

L. Carpeta « img »

A. img / icono2048.png

icono2048.png

B. img / maskable_icon.png

maskable_icon.png

C. img / maskable_icon_x128.png

maskable_icon_x128.png

D. img / maskable_icon_x192.png

maskable_icon_x192.png

E. img / maskable_icon_x384.png

maskable_icon_x384.png

F. img / maskable_icon_x48.png

maskable_icon_x48.png

G. img / maskable_icon_x512.png

maskable_icon_x512.png

H. img / maskable_icon_x72.png

maskable_icon_x72.png

I. img / maskable_icon_x96.png

maskable_icon_x96.png

J. img / screenshot_horizontal.png

screenshot_horizontal.png

K. img / screenshot_vertical.png

screenshot_vertical.png

M. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

2. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

3. lib / js / registraServiceWorkerSiEsSoportado.js

1import { muestraError } from "./muestraError.js"
2
3/**
4 * @param { string | URL } urlDeServiceWorker
5 */
6export async function registraServiceWorkerSiEsSoportado(urlDeServiceWorker) {
7 try {
8 if ("serviceWorker" in navigator) {
9 const registro = await navigator.serviceWorker.register(urlDeServiceWorker)
10 console.log(urlDeServiceWorker, "registrado.")
11 console.log(registro)
12 }
13 } catch (error) {
14 muestraError(error)
15 }
16}

N. jsconfig.json

  • 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": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

O. Resumen

  • En esta lección se presentó la estructura básica de una PWA.

5. PWA con Material Design

Versión para imprimir.

1. Introduccion

  • En esta lección se presenta una PWA con Material Design, explica como construir vistas y usar componentes.

  • La puedes probar e instalar, de preferencia con Chrome, en https://gilpgpwamd.github.io/.

2. Referencias

Sitios de Material Design
Material.io

Material Design es una guía de diseño para aplicaciones multiplataforma. La encuentras en https://m3.material.io/.

Herramienta para selección de colores
Material Theme Builder

https://m3.material.io/theme-builder/

Adaptación multiplataforma

La forma de adaptar Material Design en distintas plataformas está en https://material.io/design/platform-guidance/cross-platform-adaptation.html

3. Hazlo funcionar

4. Archivos

Haz clic en los triángulos para expandir las carpetas

5. index.html - Página principal

  • 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.

Salida

Ábrelo en otra pestaña.

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>PWA con MD</title>
11
12 <meta name="description"
13 content="Ejemplo de PWA con
14 Material Design">
15
16 <meta name="theme-color"
17 content="#fffbfe">
18 <link rel="icon" sizes="32x32"
19 href="favicon.ico">
20 <link rel="manifest"
21 href="site.webmanifest">
22
23 <!-- Configuración de todas las
24 páginas y colores con CSS. -->
25 <link rel="stylesheet"
26 href="css/estilos.css">
27
28 <!-- Para manejar customized
29 built-in elements en Safari -->
30 <script
31 src="ungap/custom-elements.js">
32 </script>
33
34 <!-- Configuración de todas las
35 páginas con JavaSript. -->
36 <script type="module"
37 src="js/configura.js"></script>
38
39</head>
40
41<body>
42
43 <md-top-app-bar
44 class="center-aligned">
45
46 <h1>PWA con MD</h1>
47
48 <button is="md-menu-button"
49 slot="navigation"
50 onclick="nav.abre()"></button>
51
52 <button type="button" title="Perfil"
53 slot="action"
54 class="md-standard-icon-button
55 avatar">
56 <span
57 class="material-symbols-outlined">
58 account_circle
59 </span>
60 </button>
61
62 </md-top-app-bar>
63
64 <main>
65 <p>
66 Esta es la página principal de la
67 app. Las X que siguen son para que
68 veas como se comporta cuando se hace
69 scroll.
70 </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 <p>x</p>
83 <p>x</p>
84 <p>x</p>
85 <p>x</p>
86 <p>x</p>
87 <p>x</p>
88 <p>x</p>
89 <p>x</p>
90 <p>x</p>
91 <p>x</p>
92 <p>x</p>
93 <p>x</p>
94 </main>
95
96 <nav-drw id="nav"></nav-drw>
97
98</body>
99
100</html>

Solo para los más rudos

6. secundaria.html - Vista secundaria

  • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Vista secundaria - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <!-- Configuración de todas las
21 páginas y colores con CSS. -->
22 <link rel="stylesheet"
23 href="css/estilos.css">
24
25 <!-- Para manejar customized
26 built-in elements en Safari -->
27 <script
28 src="ungap/custom-elements.js">
29 </script>
30
31 <!-- Configuración de todas las
32 páginas con JavaSript. -->
33 <script type="module"
34 src="js/configura.js"></script>
35
36</head>
37
38<body>
39
40 <md-top-app-bar class="medium"
41 headline="h1Headline">
42
43 <h1>Secundaria</h1>
44
45 <button is="md-menu-button"
46 slot="navigation"
47 onclick="nav.abre()"></button>
48
49 <button type="button"
50 title="Agregar" slot="action"
51 class="md-standard-icon-button">
52 <span
53 class="material-symbols-outlined">
54 add
55 </span>
56 </button>
57
58 <button type="button"
59 title="Editar" slot="action"
60 class="md-standard-icon-button">
61 <span
62 class="material-symbols-outlined">
63 edit
64 </span>
65 </button>
66
67 <button is="md-overflow-button"
68 slot="action"
69 onclick="overflow.alterna(this)">
70 </button>
71
72 </md-top-app-bar>
73
74 <h1 id="h1Headline">
75 Página secundaria
76 </h1>
77
78 <main>
79 <p>
80 Esta es una página secundaria de la
81 app. Las X que siguen son para que
82 veas como se comporta cuando se hace
83 scroll.
84 </p>
85 <p>x</p>
86 <p>x</p>
87 <p>x</p>
88 <p>x</p>
89 <p>x</p>
90 <p>x</p>
91 <p>x</p>
92 <p>x</p>
93 <p>x</p>
94 <p>x</p>
95 <p>x</p>
96 <p>x</p>
97 <p>x</p>
98 <p>x</p>
99 <p>x</p>
100 <p>x</p>
101 <p>x</p>
102 <p>x</p>
103 <p>x</p>
104 <p>x</p>
105 <p>x</p>
106 <p>x</p>
107 <p>x</p>
108 </main>
109
110 <md-overflow-menu id="overflow">
111
112 <button type="button">
113 <span
114 class="material-symbols-outlined">
115 star
116 </span>
117 Marcar favorito
118 </button>
119
120 <button type="button">
121 <span
122 class="material-symbols-outlined">
123 delete
124 </span>
125 Eliminar
126 </button>
127
128 </md-overflow-menu>
129
130 <nav-drw id="nav"></nav-drw>
131
132</body>
133
134</html>

Solo para los más rudos

7. iconos.html - Íconos oficiales

  • 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.

Salida

Ábrelo en otra pestaña.

1 <!DOCTYPE html>
2 <html lang="es">
3
4 <head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Íconos - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28 </head>
29
30 <body>
31
32 <md-top-app-bar>
33
34 <h1>Íconos</h1>
35
36 <button is="md-menu-button"
37 slot="navigation"
38 onclick="nav.abre()"></button>
39
40 </md-top-app-bar>
41
42 <main>
43
44 <button type="button"
45 class="md-standard-icon-button">
46 <span
47 class="material-symbols-outlined">
48 favorite
49 </span>
50 </button>
51
52 <button type="button"
53 class="md-standard-icon-button"
54 disabled>
55 <span
56 class="material-symbols-outlined">
57 bolt
58 </span>
59 </button>
60
61 <a href="https://google.com"
62 class="md-standard-icon-button">
63 <span
64 class="material-symbols-outlined">
65 star
66 </span></a>
67
68 <span
69 class="material-symbols-outlined">
70 thumb_up
71 </span>
72
73 <button type="button"
74 class="md-fab-primary"
75 style="position: fixed;
76 bottom: 16px;
77 right: 16px;">
78 <span
79 class="material-symbols-outlined">
80 add
81 </span>
82 </button>
83
84 </main>
85
86 <nav-drw id="nav"></nav-drw>
87
88 </body>
89
90 </html>

Solo para los más rudos

8. botones.html - Botones

  • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Botones - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20
21 <link rel="stylesheet"
22 href="css/estilos.css">
23 <script
24 src="ungap/custom-elements.js">
25 </script>
26 <script type="module"
27 src="js/configura.js"></script>
28
29</head>
30
31<body>
32
33 <md-top-app-bar>
34
35 <h1>Botones</h1>
36
37 <button is="md-menu-button"
38 slot="navigation"
39 onclick="nav.abre()"></button>
40
41 </md-top-app-bar>
42
43 <main>
44
45 <p>
46
47 <button
48 class="md-filled-button">
49 Primario
50 </button>
51
52 <button
53 class="md-outline-button">
54 Secundario
55 </button>
56 </p>
57
58 </main>
59
60 <nav-drw id="nav"></nav-drw>
61
62</body>
63
64</html>

Solo para los más rudos

9. campos.html - Campos de texto

  • 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 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.

  • Las etiquetas deben ser label o span y deben colocarse inmediatamente después del elemento que realiza la captura.

  • Los textos de ayuda deben ser elementos de tipo small que se colocan después de la etiqueta.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Campos de texto - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28</head>
29
30<body>
31
32 <md-top-app-bar
33 headline="headline">
34
35 <h1>Texto</h1>
36
37 <button is="md-menu-button"
38 slot="navigation"
39 onclick="nav.abre()"></button>
40
41 </md-top-app-bar>
42
43 <h1 id="headline">Campos de texto</h1>
44
45 <main>
46
47
48 <!-- Usa class="float" cuando
49 quieras que la etiqueta esté
50 arriba todo el tiempo. -->
51 <p class="md-filled-text-field float">
52 <label for="saludo">Saludo</label>
53 <output id="saludo">Saludo</output>
54 </p>
55
56 <p class="md-filled-text-field">
57 <input id="nombre" required
58 placeholder="Nombre*">
59 <label for="nombre">Nombre*</label>
60 <small>*Requerido</small>
61 </p>
62
63 <p class="md-filled-text-field">
64 <input id="email" type="email"
65 placeholder="Email">
66 <label for="email" accesskey="M">
67 Email
68 </label>
69 </p>
70
71 <p class="md-filled-text-field float">
72 <input id="fecha" type="date"
73 placeholder="Fecha">
74 <label for="fecha">Fecha</label>
75 </p>
76
77 <p class="md-filled-text-field">
78 <textarea id="direccion"
79 placeholder="Dirección"
80 rows="3"></textarea>
81 <label for="direccion">
82 Dirección
83 </label>
84 </p>
85
86 </main>
87
88 <nav-drw id="nav"></nav-drw>
89
90</body>
91
92</html>

Solo para los más rudos

10. select.html - Selects

  • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Select - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28</head>
29
30<body>
31
32 <form>
33
34 <md-top-app-bar>
35
36 <h1>Select</h1>
37
38 <button is="md-menu-button"
39 slot="navigation"
40 onclick="nav.abre()"></button>
41
42 </md-top-app-bar>
43
44 <main>
45
46 <p class="md-filled-text-field">
47 <md-select-menu name="grupo"
48 required
49 aria-labelledby="etiquetaGrupo"
50 options="opcionesDeGrupo"
51 value="IC21">
52 </md-select-menu>
53 <span id="etiquetaGrupo"
54 accesskey="G">
55 Grupo*
56 </span>
57 <small>*Requerido</small>
58 </p>
59
60 <p>
61 <button class="md-filled-button"
62 style="width: 100%;">
63 Enviar
64 </button>
65 </p>
66
67 </main>
68
69 <md-options-menu
70 id="opcionesDeGrupo"
71 aria-label="Opciones de grupo">
72
73 <span data-value=""
74 title="Selecciona opción">
75 </span>
76
77 <span data-value="IC21">
78 IC-21
79 </span>
80
81 <span data-value="IC22">
82 IC-22
83 </span>
84
85 <span data-value="IC23">
86 IC-23
87 </span>
88
89 </md-options-menu>
90
91 <nav-drw id="nav"></nav-drw>
92
93 </form>
94
95</body>
96
97</html>

Solo para los más rudos

11. interruptor.html - Interruptores

  • Usa un elemento input con el atribto type="checkbox" y class="md-switch".

Salida

Ábrelo en otra pestaña.

Interruptor en Android
Interruptor en Android
Interruptor en iOS
Interruptor en iOS
1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Interruptores - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20
21 <link rel="stylesheet"
22 href="css/estilos.css">
23 <script
24 src="ungap/custom-elements.js">
25 </script>
26 <script type="module"
27 src="js/configura.js"></script>
28
29</head>
30
31<body>
32
33 <md-top-app-bar>
34
35 <h1>Interruptores</h1>
36
37 <button is="md-menu-button"
38 slot="navigation"
39 onclick="nav.abre()"></button>
40
41 </md-top-app-bar>
42
43 <main>
44 <p>
45 <label accesskey="R">
46 Muestra resultados
47 <input type="checkbox"
48 class="md-switch">
49 </label>
50 </p>
51 </main>
52
53 <nav-drw id="nav"></nav-drw>
54
55</body>
56
57</html>

Solo para los más rudos

12. slider.html

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Sliders - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28</head>
29
30<body>
31
32 <md-top-app-bar>
33
34 <h1>Sliders</h1>
35
36 <button is="md-menu-button"
37 slot="navigation"
38 onclick="nav.abre()"></button>
39
40 </md-top-app-bar>
41
42 <main>
43
44 <md-slider-field>
45 <label>Rango</label>
46 <input type="range"
47 slot="slider" min="1"
48 max="10">
49 <small slot="supporting">
50 Calcúlalo bien
51 </small>
52 </md-slider-field>
53
54 </main>
55
56 <nav-drw id="nav"></nav-drw>
57
58</body>
59
60</html>

Solo para los más rudos

13. segmentado.html - Botones segmentados

  • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Botón segmentado - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28</head>
29
30<body>
31
32 <md-top-app-bar headline="headline">
33
34 <h1>Segmentado</h1>
35
36 <button is="md-menu-button"
37 slot="navigation"
38 onclick="nav.abre()"></button>
39
40 </md-top-app-bar>
41
42 <h1 id="headline">
43 Botón segmentado
44 </h1>
45
46 <p class="md-segmented-button">
47
48 <input id="bueno" type="radio"
49 name="estado" checked value="2">
50 <label for="bueno">
51 <span
52 class="material-symbols-outlined">
53 done
54 </span>
55 Bueno
56 </label>
57
58 <input id="intermedio" type="radio"
59 name="estado" value="1">
60 <label for="intermedio">
61 <span
62 class="material-symbols-outlined">
63 done
64 </span>
65 Intermedio
66 </label>
67
68 <input id="malo" type="radio"
69 name="estado" value="0">
70 <label for="malo">
71 <span
72 class="material-symbols-outlined">
73 done
74 </span>
75 Malo
76 </label>
77
78 </p>
79
80 <nav-drw id="nav"></nav-drw>
81
82</body>
83
84</html>

Solo para los más rudos

14. one-line.html - Listas con elementos one-line

  • 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:

      Un pequeño ícono cuadrado
      • 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.

      Una imagen cuadrada
      • 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.

      Una imagen rectangular
      • 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.

      El avatar de un usuario (se muestra dentro de un círculo)
      • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width">
7
8 <title>
9 Listas one-line - PWA con MD
10 </title>
11
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15
16 <link rel="stylesheet" href="css/estilos.css">
17 <script src="ungap/custom-elements.js"></script>
18 <script type="module" src="js/configura.js"></script>
19
20</head>
21
22<body>
23
24 <md-top-app-bar headline="headline">
25
26 <h1>one-line</h1>
27
28 <button is="md-menu-button" slot="navigation" onclick="nav.abre()"></button>
29
30 </md-top-app-bar>
31
32 <h1 id="headline">Listas one-line</h1>
33
34 <main>
35
36 <ul class="md-list">
37
38 <li class="md-one-line">
39 <span class="headline">
40 Ciudad de México
41 </span>
42 </li>
43
44 <li class="md-one-line">
45 <span class="headline">
46 Lorem ipsum dolor sit amet consectetur adipisicing elit.
47 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
48 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
49 alias!
50 </span>
51 </li>
52
53 <li class="md-one-line icon">
54 <span class="material-symbols-outlined">
55 account_balance
56 </span>
57 <span class="headline">
58 Atenas
59 </span>
60 </li>
61
62 <li class="md-one-line avatar">
63 <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg">
64 <span class="headline">
65 Ana
66 </span>
67 </li>
68
69 <li class="md-one-line image">
70 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
71 <span class="headline">
72 Neza
73 </span>
74 </li>
75
76 <li class="md-one-line video">
77 <img alt="Ciudad de San Francisco"
78 src="img/pexels-craig-dennis-3701822.jpg">
79 <span class="headline">
80 San Francisco
81 </span>
82 </li>
83
84 <li class="md-one-line video">
85 <img alt="Ciudad de San Francisco" 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 alias!
91 </span>
92 </li>
93
94 <li>
95 <a class="md-one-line image" target="_blank" rel=”noopener noreferrer”
96 href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/">
97 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
98 <span class="headline">
99 Neza Link
100 </span>
101 </a>
102 </li>
103
104 </ul>
105
106 <footer>
107 <ul>
108 <li>
109 <p>
110 <small>
111 <a target="_blank" rel=”noopener noreferrer”
112 href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/">
113 La foto de la niña es de Moises Patrício, publicada en el sitio Pexels.
114 Haz clic en este hipervínculo para más información.
115 </a>
116 </small>
117 </p>
118 </li>
119 <li>
120 <p>
121 <small>
122 <a target="_blank" rel=”noopener noreferrer” 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=”noopener 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 id="nav"></nav-drw>
146
147</body>
148
149</html>

Solo para los más rudos

15. two-line.html - Listas con elementos two-line

  • 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:

      Un pequeño ícono cuadrado
      • 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.

      Una imagen cuadrada
      • 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.

      Una imagen rectangular
      • 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.

      El avatar de un usuario (se muestra dentro de un círculo)
      • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width">
7
8 <title>
9 Listas two-line - PWA con MD
10 </title>
11
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15
16 <link rel="stylesheet" href="css/estilos.css">
17 <script src="ungap/custom-elements.js"></script>
18 <script type="module" src="js/configura.js"></script>
19
20</head>
21
22<body>
23
24 <md-top-app-bar headline="headline">
25
26 <h1>two-line</h1>
27
28 <button is="md-menu-button" slot="navigation" onclick="nav.abre()"></button>
29
30 </md-top-app-bar>
31
32 <h1 id="headline">
33 Listas two-line
34 </h1>
35
36 <main>
37
38 <ul class="md-list">
39
40 <li class="md-two-line">
41 <span class="headline">
42 Ciudad de México 2
43 </span>
44 <span class="supporting">
45 Lorem ipsum dolor sit amet consectetur adipisicing elit.
46 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
47 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
48 alias!
49 </span>
50 </li>
51
52 <li class="md-two-line icon">
53 <span class="material-symbols-outlined">
54 account_balance
55 </span>
56 <span class="headline">
57 Atenas 2
58 </span>
59 <span class="supporting">
60 Lorem ipsum dolor sit amet consectetur adipisicing elit.
61 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
62 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
63 alias!
64 </span>
65 </li>
66
67 <li class="md-two-line avatar">
68 <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg">
69 <span class="headline">
70 Ana 2
71 </span>
72 <span class="supporting">
73 Lorem ipsum dolor sit amet consectetur adipisicing elit.
74 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
75 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
76 alias!
77 </span>
78 </li>
79
80 <li class="md-two-line image">
81 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
82 <span class="headline">
83 Neza 2
84 </span>
85 <span class="supporting">
86 Lorem ipsum dolor sit amet consectetur adipisicing elit.
87 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
88 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
89 alias!
90 </span>
91 </li>
92
93 <li class="md-two-line video">
94 <img alt="Ciudad de San Francisco"
95 src="img/pexels-craig-dennis-3701822.jpg">
96 <span class="headline">
97 San Francisco 2
98 </span>
99 <span class="supporting">
100 Lorem ipsum dolor sit amet consectetur adipisicing elit.
101 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
102 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
103 alias!
104 </span>
105 </li>
106
107 <li>
108 <a class="md-two-line image" target="_blank" rel=”noopener noreferrer”
109 href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/">
110 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
111 <span class="headline">
112 Neza Link 2
113 </span>
114 <span class="supporting">
115 Lorem ipsum dolor sit amet consectetur adipisicing elit.
116 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
117 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
118 alias!
119 </span>
120 </a>
121 </li>
122
123 </ul>
124
125 <footer>
126 <ul>
127 <li>
128 <p>
129 <small>
130 <a
131 target="_blank" rel=”noopener noreferrer”
132 href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/">
133 La foto de la niña es de Moises Patrício, publicada en el sitio Pexels.
134 Haz clic en este hipervínculo para más información.
135 </a>
136 </small>
137 </p>
138 </li>
139 <li>
140 <p>
141 <small>
142 <a target="_blank" rel=”noopener noreferrer”
143 href="https://www.pinterest.com.mx/ludresi/">
144 La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio
145 Pinterest. Haz clic en este hipervínculo para más información.
146 </a>
147 </small>
148 </p>
149 </li>
150 <li>
151 <p>
152 <small>
153 <a target="_blank" rel=”noopener noreferrer”
154 href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/">
155 La foto del puente de San Francisco es de Craig Dennis, publicada en el
156 sitio Pexels. Haz clic en este hipervínculo para más información.
157 </a>
158 </small>
159 </p>
160 </li>
161 </ul>
162 </footer>
163
164 </main>
165
166 <nav-drw id="nav"></nav-drw>
167
168</body>
169
170</html>

Solo para los más rudos

16. three-line.html - Listas con elementos three-line

  • 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:

      Un pequeño ícono cuadrado
      • 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.

      Una imagen cuadrada
      • 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.

      Una imagen rectangular
      • 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.

      El avatar de un usuario (se muestra dentro de un círculo)
      • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width">
7
8 <title>
9 Listas tree-line - PWA con MD
10 </title>
11
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15
16 <link rel="stylesheet" href="css/estilos.css">
17 <script src="ungap/custom-elements.js"></script>
18 <script type="module" src="js/configura.js"></script>
19
20</head>
21
22<body>
23
24 <md-top-app-bar
25 headline="headline">
26
27 <h1>tree-line</h1>
28
29 <button is="md-menu-button" slot="navigation" onclick="nav.abre()"></button>
30
31 </md-top-app-bar>
32
33 <h1 id="headline">
34 Listas tree-line
35 </h1>
36
37 <main>
38
39 <ul class="md-list">
40
41 <li class="md-three-line">
42 <span class="headline">
43 Ciudad de México 3
44 </span>
45 <span class="supporting">
46 Lorem ipsum dolor sit amet consectetur adipisicing elit.
47 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
48 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
49 alias!
50 </span>
51 </li>
52
53 <li class="md-three-line icon">
54 <span class="material-symbols-outlined">
55 account_balance
56 </span>
57 <span class="headline">
58 Atenas 3
59 </span>
60 <span class="supporting">
61 Lorem ipsum dolor sit amet consectetur adipisicing elit.
62 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
63 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
64 alias!
65 </span>
66 </li>
67
68 <li class="md-three-line avatar">
69 <img alt="Avatar de Ana" src="img/pexels-moises-patrício-10961948.jpg">
70 <span class="headline">
71 Ana 3
72 </span>
73 <span class="supporting">
74 Lorem ipsum dolor sit amet consectetur adipisicing elit.
75 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
76 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
77 alias!
78 </span>
79 </li>
80
81 <li class="md-three-line image">
82 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
83 <span class="headline">
84 Neza 3
85 </span>
86 <span class="supporting">
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 alias!
91 </span>
92 </li>
93
94 <li class="md-three-line video">
95 <img alt="Ciudad de San Francisco"
96 src="img/pexels-craig-dennis-3701822.jpg">
97 <span class="headline">
98 San Francisco 3
99 </span>
100 <span class="supporting">
101 Lorem ipsum dolor sit amet consectetur adipisicing elit.
102 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
103 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
104 alias!
105 </span>
106 </li>
107
108 <li>
109 <a class="md-three-line image"
110 target="_blank" rel=”noopener noreferrer”
111 href="https://culturacolectiva.com/historia/ciudad-neza-su-historia-en-fotografias/">
112 <img alt="Coyote de Neza" src="img/Escultura_de_coyote.jpeg">
113 <span class="headline">
114 Neza Link 3
115 </span>
116 <span class="supporting">
117 Lorem ipsum dolor sit amet consectetur adipisicing elit.
118 Laboriosam eaque unde voluptates tempore ad suscipit libero, saepe neque
119 illo amet, eos ea similique quia, maiores tenetur modi nobis expedita
120 alias!
121 </span>
122 </a>
123 </li>
124
125 </ul>
126
127 <footer>
128 <ul>
129 <li>
130 <p>
131 <small>
132 <a target="_blank" rel=”noopener noreferrer”
133 href="https://www.pexels.com/es-es/foto/nina-mono-cara-sonriente-10961948/">
134 La foto de la niña es de Moises Patrício, publicada en el sitio Pexels.
135 Haz clic en este hipervínculo para más información.
136 </a>
137 </small>
138 </p>
139 </li>
140 <li>
141 <p>
142 <small>
143 <a target="_blank" rel=”noopener noreferrer” href="https://www.pinterest.com.mx/ludresi/">
144 La foto del Coyote de Neza es de Ludres Isan, publicada en el sitio
145 Pinterest. Haz clic en este hipervínculo para más información.
146 </a>
147 </small>
148 </p>
149 </li>
150 <li>
151 <p>
152 <small>
153 <a target="_blank" rel=”noopener noreferrer”
154 href="https://www.pexels.com/es-es/foto/puente-golden-gate-san-francisco-california-3701822/">
155 La foto del puente de San Francisco es de Craig Dennis, publicada en el
156 sitio Pexels. Haz clic en este hipervínculo para más información.
157 </a>
158 </small>
159 </p>
160 </li>
161 </ul>
162 </footer>
163
164 </main>
165
166 <nav-drw id="nav"></nav-drw>
167
168</body>
169
170</html>

Solo para los más rudos

17. tarjetas.html - Tarjetas

  • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width">
7
8 <title>
9 Tarjetas - PWA con MD
10 </title>
11
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15
16 <link rel="stylesheet" href="css/estilos.css">
17 <script src="ungap/custom-elements.js"></script>
18 <script type="module" src="js/configura.js"></script>
19
20</head>
21
22<body>
23
24 <md-top-app-bar>
25 <h1>Tarjetas</h1>
26
27 <button is="md-menu-button" slot="navigation" onclick="nav.abre()"></button>
28
29 </md-top-app-bar>
30
31 <div class="md-cards">
32
33 <a target="_blank" rel=”noopener noreferrer”
34 href="https://www.pexels.com/es-es/foto/lobo-blanco-y-negro-397857/">
35 <figure>
36 <img alt="Lobo" src="img/pexels-steve-397857.jpg">
37 </figure>
38 <span class="headline">
39 Lobo
40 </span>
41 <span class="supporting">
42 Foto de Steve en Pexels.
43 </span>
44 </a>
45
46 <a target="_blank" rel=”noopener noreferrer”
47 href="https://www.pexels.com/es-es/foto/foto-de-buho-ural-3732453/">
48 <figure>
49 <img alt="Buho" src="img/pexels-erik-karits-3732453.jpg">
50 </figure>
51 <span class="headline">
52 Buho
53 </span>
54 <span class="supporting">
55 Foto de Erik Karits en Pexels
56 </span>
57 </a>
58
59 <a target="_blank" rel=”noopener noreferrer”
60 href="https://www.pexels.com/es-es/foto/perro-de-pelo-corto-marron-y-blanco-acostado-3978352/">
61 <figure>
62 <img alt="Perro" src="img/pexels-creative-workshop-3978352.jpg">
63 </figure>
64 <span class="headline">
65 Perro
66 </span>
67 <span class="supporting">
68 Foto de Creative Workshop en Pexels
69 </span>
70 </a>
71
72 <a target="_blank" rel=”noopener noreferrer”
73 href="https://www.pexels.com/es-es/foto/gatito-gris-en-bolsa-de-papel-plateada-141496/">
74 <figure>
75 <img alt="Gato" src="img/pexels-vadim-b-141496.jpg">
76 </figure>
77 <span class="headline">
78 Gato
79 </span>
80 <span class="supporting">
81 Foto de Vadim B en Pexels
82 </span>
83 </a>
84
85 <a target="_blank" rel=”noopener noreferrer”
86 href="https://www.pexels.com/es-es/foto/leon-marron-2270848/">
87 <figure>
88 <img alt="León" src="img/pexels-ralph-2270848.jpg">
89 </figure>
90 <span class="headline">
91 León
92 </span>
93 <span class="supporting">
94 Foto de Ralph en Pexels
95 </span>
96 </a>
97
98 <a target="_blank" rel=”noopener noreferrer”
99 href="https://www.pexels.com/es-es/foto/oso-cafe-35435/">
100 <figure>
101 <img alt="Oso" src="img/pexels-rasmus-svinding-35435.jpg">
102 </figure>
103 <span class="headline">
104 Oso
105 </span>
106 <span class="supporting">
107 Foto de Rasmus Svinding en Pexels
108 </span>
109 </a>
110
111 <a target="_blank" rel=”noopener noreferrer”
112 href="https://www.pexels.com/es-es/foto/animal-perro-mono-hierba-10226903/">
113 <figure>
114 <img alt="Coyote" src="img/pexels-esteban-arango-10226903.jpg">
115 </figure>
116 <span class="headline">
117 Coyote
118 </span>
119 <span class="supporting">
120 Foto de Esteban Arango en Pexels
121 </span>
122 </a>
123
124 </div>
125
126 <nav-drw id="nav"></nav-drw>
127
128</body>
129
130</html>

Solo para los más rudos

18. Cajón de navegación

  • 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". Su método onclick abre el cajón de navegación.

  • Coloca nav-drw hasta el final y ponle su id".

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Vista secundaria - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <!-- Configuración de todas las
21 páginas y colores con CSS. -->
22 <link rel="stylesheet"
23 href="css/estilos.css">
24
25 <!-- Para manejar customized
26 built-in elements en Safari -->
27 <script
28 src="ungap/custom-elements.js">
29 </script>
30
31 <!-- Configuración de todas las
32 páginas con JavaSript. -->
33 <script type="module"
34 src="js/configura.js"></script>
35
36</head>
37
38<body>
39
40 <md-top-app-bar class="medium"
41 headline="h1Headline">
42
43 <h1>Secundaria</h1>
44
45 <button is="md-menu-button"
46 slot="navigation"
47 onclick="nav.abre()"></button>
48
49 <button type="button"
50 title="Agregar" slot="action"
51 class="md-standard-icon-button">
52 <span
53 class="material-symbols-outlined">
54 add
55 </span>
56 </button>
57
58 <button type="button"
59 title="Editar" slot="action"
60 class="md-standard-icon-button">
61 <span
62 class="material-symbols-outlined">
63 edit
64 </span>
65 </button>
66
67 <button is="md-overflow-button"
68 slot="action"
69 onclick="overflow.alterna(this)">
70 </button>
71
72 </md-top-app-bar>
73
74 <h1 id="h1Headline">
75 Página secundaria
76 </h1>
77
78 <main>
79 <p>
80 Esta es una página secundaria de la
81 app. Las X que siguen son para que
82 veas como se comporta cuando se hace
83 scroll.
84 </p>
85 <p>x</p>
86 <p>x</p>
87 <p>x</p>
88 <p>x</p>
89 <p>x</p>
90 <p>x</p>
91 <p>x</p>
92 <p>x</p>
93 <p>x</p>
94 <p>x</p>
95 <p>x</p>
96 <p>x</p>
97 <p>x</p>
98 <p>x</p>
99 <p>x</p>
100 <p>x</p>
101 <p>x</p>
102 <p>x</p>
103 <p>x</p>
104 <p>x</p>
105 <p>x</p>
106 <p>x</p>
107 <p>x</p>
108 </main>
109
110 <md-overflow-menu id="overflow">
111
112 <button type="button">
113 <span
114 class="material-symbols-outlined">
115 star
116 </span>
117 Marcar favorito
118 </button>
119
120 <button type="button">
121 <span
122 class="material-symbols-outlined">
123 delete
124 </span>
125 Eliminar
126 </button>
127
128 </md-overflow-menu>
129
130 <nav-drw id="nav"></nav-drw>
131
132</body>
133
134</html>

Solo para los más rudos

19. navtab.html - Navegación con pestañas scrollable

  • 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

  • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Pestañas scrollable - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28</head>
29
30<body>
31
32 <md-top-app-bar adicional="tab"
33 headline="headline">
34
35 <h1>
36 Pestañas scrollable
37 </h1>
38
39 <button type="button" title="Agregar"
40 slot="action"
41 class="md-standard-icon-button">
42 <span
43 class="material-symbols-outlined">
44 add
45 </span>
46 </button>
47 <button type="button" title="Editar"
48 slot="action"
49 class="md-standard-icon-button">
50 <span
51 class="material-symbols-outlined">
52 edit
53 </span>
54 </button>
55
56 </md-top-app-bar>
57
58 <h1 id="headline">
59 Pestañas scrollable
60 </h1>
61
62 <nav-tab-scrollable
63 id="tab"></nav-tab-scrollable>
64
65 <main>
66 <p>
67 Esta página usa navegación por
68 pestañas fijas. Las X que siguen son
69 para que veas como se comporta
70 cuando se hace scroll.
71 </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 <p>x</p>
83 <p>x</p>
84 <p>x</p>
85 <p>x</p>
86 <p>x</p>
87 <p>x</p>
88 <p>x</p>
89 <p>x</p>
90 <p>x</p>
91 <p>x</p>
92 <p>x</p>
93 <p>x</p>
94 <p>x</p>
95 </main>
96
97</body>
98
99</html>

Solo para los más rudos

20. navTabFixed.html - Navegación con pestañas fijas

  • 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

  • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Pestañas fijas - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28</head>
29
30<body>
31
32 <md-top-app-bar adicional="tab"
33 headline="headline">
34
35 <h1>fijas</h1>
36
37 <button type="button" title="Agregar"
38 slot="action"
39 class="md-standard-icon-button">
40 <span
41 class="material-symbols-outlined">
42 add
43 </span>
44 </button>
45 <button type="button" title="Editar"
46 slot="action"
47 class="md-standard-icon-button">
48 <span
49 class="material-symbols-outlined">
50 edit
51 </span>
52 </button>
53
54 </md-top-app-bar>
55
56 <h1 id="headline">Pestañas fijas</h1>
57
58 <nav-tab-fixed
59 id="tab"></nav-tab-fixed>
60
61 <main>
62 <p>
63 Esta página usa navegación por
64 pestañas filas. Las X que siguen son
65 para que veas como se comporta
66 cuando se hace scroll.
67 </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 <p>x</p>
83 <p>x</p>
84 <p>x</p>
85 <p>x</p>
86 <p>x</p>
87 <p>x</p>
88 <p>x</p>
89 <p>x</p>
90 <p>x</p>
91 </main>
92
93</body>
94
95</html>

21. navbar.html - Barra de navegación

  • 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

  • Coloca nav-bar al final de tu forma.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Barra de Navegación - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28</head>
29
30<body style="padding-bottom: 80px;">
31
32 <md-top-app-bar headline="headline">
33
34 <h1>navegación</h1>
35
36 <button type="button" title="Agregar"
37 slot="action"
38 class="md-standard-icon-button">
39 <span
40 class="material-symbols-outlined">
41 add
42 </span>
43 </button>
44 <button type="button" title="Editar"
45 slot="action"
46 class="md-standard-icon-button">
47 <span
48 class="material-symbols-outlined">
49 edit
50 </span>
51 </button>
52
53 </md-top-app-bar>
54
55 <h1 id="headline">
56 Barra de navegación
57 </h1>
58
59 <main>
60 <p>
61 Esta página usa barra de navegación.
62 Las X que siguen son para que veas
63 como se comporta cuando se hace
64 scroll.
65 </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 <p>x</p>
83 <p>x</p>
84 <p>x</p>
85 <p>x</p>
86 <p>x</p>
87 <p>x</p>
88 <p>x</p>
89 </main>
90
91 <nav-bar></nav-bar>
92
93</body>
94
95</html>

Solo para los más rudos

22. formulario.html - Formulario típico

  • 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.

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport"
7 content="width=device-width">
8
9 <title>
10 Formulario - PWA con MD
11 </title>
12
13 <meta name="theme-color"
14 content="#fffbfe">
15 <link rel="icon" sizes="32x32"
16 href="favicon.ico">
17 <link rel="manifest"
18 href="site.webmanifest">
19
20 <link rel="stylesheet"
21 href="css/estilos.css">
22 <script
23 src="ungap/custom-elements.js">
24 </script>
25 <script type="module"
26 src="js/configura.js"></script>
27
28</head>
29
30<body>
31
32 <form id="form" novalidate
33 onsubmit="procesa(event)">
34
35 <md-top-app-bar>
36
37 <h1>Formulario</h1>
38
39 <button is="md-menu-button"
40 slot="navigation"
41 onclick="nav.abre()"></button>
42
43 </md-top-app-bar>
44
45 <main>
46
47 <p class="md-filled-text-field">
48 <md-select-menu id="selectGenero"
49 required
50 aria-labelledby="etiquetaGenero"
51 options="opcionesDeGenero"
52 oninput="copiaMensajes()">
53 </md-select-menu>
54 <span id="etiquetaGenero"
55 accesskey="G">
56 Género*
57 </span>
58 <small id="supportingGenero">
59 * Obligatorio
60 </small>
61 </p>
62
63 <p>
64 <button class="md-filled-button"
65 style="width: 100%;">
66 Recomendar
67 </button>
68 </p>
69
70 </main>
71
72 <md-options-menu id="opcionesDeGenero"
73 aria-label="Opciones de género">
74
75 <span data-value=""
76 title="Selecciona género">
77 </span>
78
79 <span data-value="pop">
80 Pop
81 </span>
82
83 <span data-value="reg">
84 Reguetón
85 </span>
86
87 </md-options-menu>
88
89 <nav-drw id="nav"></nav-drw>
90
91 </form>
92
93 <script type="module">
94 import {
95 muestraError
96 } from "./lib/js/muestraError.js"
97 import {
98 muestraTextoDeAyuda
99 } from "./lib/js/muestraTextoDeAyuda.js"
100
101 function copiaMensajes() {
102 muestraTextoDeAyuda(
103 selectGenero, supportingGenero,
104 "* Obligatorio")
105 }
106 window["copiaMensajes"] =
107 copiaMensajes
108
109 /**
110 * @param {SubmitEvent} evt
111 */
112 function procesa(evt) {
113 evt.preventDefault()
114 try {
115 copiaMensajes()
116 if (
117 selectGenero.validity.valid) {
118 const genero =
119 selectGenero.value
120 const resultado =
121 recomienda(genero)
122 alert(resultado)
123 }
124 } catch (e) {
125 muestraError(e)
126 }
127 }
128 window["procesa"] = procesa
129
130 /** @param {string} genero */
131 function recomienda(genero) {
132 if (genero === "pop") {
133 return "Para el pop te " +
134 "recomiendo a Dua Lipa."
135 } else if (genero === "reg") {
136 return "Para el reguetón te " +
137 "recomiendo a Bad Bunny."
138 }
139 }
140 </script>
141
142</body>
143
144</html>

23. ayuda.html - Vista de ayuda

Salida

Ábrelo en otra pestaña.

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width">
7
8 <title>
9 Ayuda - PWA con MD
10 </title>
11
12 <meta name="theme-color" content="#fffbfe">
13 <link rel="icon" sizes="32x32" href="favicon.ico">
14 <link rel="manifest" href="site.webmanifest">
15
16 <link rel="stylesheet" href="css/estilos.css">
17 <script src="ungap/custom-elements.js"></script>
18 <script type="module" src="js/configura.js"></script>
19
20</head>
21
22<body>
23
24 <md-top-app-bar>
25
26 <h1>Ayuda</h1>
27
28 <button is="md-menu-button" slot="navigation"
29 onclick="nav.abre()"></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 rel="license noopener noreferrer” target="_blank"
75 href="http://creativecommons.org/licenses/by/4.0/">
76 Licencia Creative Commons Atribución 4.0
77 Internacional</a> </span>
78 </li>
79 <li>
80 <a class="md-three-line"
81 target="_blank" rel=”noopener noreferrer”
82 href="https://fonts.google.com/icons">
83 <span class="headline">
84 También usa Material Symbols
85 </span>
86 <span class="supporting">
87 Desarrollada por Google bajo licencia Apache 2.0
88 </span>
89 </a>
90 </li>
91 </ul>
92
93 </main>
94
95 <nav-drw id="nav"></nav-drw>
96
97</body>
98
99</html>

24. Carpeta « js »

A. js / configura.js

1/** Barra de navegación. */
2import "./nav-drw.js"
3import "./nav-tab-scrollable.js"
4import "./nav-tab-fixed.js"
5import "./nav-bar.js"
6/** Elementos utilizados */
7import "../lib/js/custom/md-menu-button.js"
8import "../lib/js/custom/md-options-menu.js"
9import "../lib/js/custom/md-overflow-button.js"
10import "../lib/js/custom/md-overflow-menu.js"
11import "../lib/js/custom/md-select-menu.js"
12import "../lib/js/custom/md-top-app-bar.js"
13import "../lib/js/custom/md-slider-field.js"
14import {
15 registraServiceWorkerSiEsSoportado
16} from "../lib/js/registraServiceWorkerSiEsSoportado.js"
17
18registraServiceWorkerSiEsSoportado("sw.js")
19
20/* Evita los cambios de apariencia al cargar estilos y custom elements, que
21 * son conocidos como Flash Of Unstyled Content (fouc). */
22addEventListener("load", () => document.body.classList.add("fouc"))

B. js / nav-bar.js

1import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js"
2
3export 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 ${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
33customElements.define("nav-bar", NavBar)

C. js / nav-drw.js

1import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js"
2import { MdNavigationDrawer } from "../lib/js/custom/MdNavigationDrawer.js"
3
4export 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
106customElements.define("nav-drw", NavDrw)

D. js / nav-tab-fixed.js

1import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js"
2
3export 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 ${resaltaSiEstasEn(["/navTabFixed.html"])} href="navTabFixed.html">
20 <span class="material-symbols-outlined">tabs</span>
21 Pestañas fijas
22 </a>
23
24 <a ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html">
25 <span class="material-symbols-outlined">bottom_navigation</span>
26 Barra de navegación
27 </a>`
28 }
29
30}
31
32customElements.define("nav-tab-fixed", NavTabFixed)

E. js / nav-tab-scrollable.js

1import { resaltaSiEstasEn } from "../lib/js/resaltaSiEstasEn.js"
2
3export class NavTabScrollable extends HTMLElement {
4
5 connectedCallback() {
6 this.classList.add("md-tab", "scrollable")
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(["/secundaria.html"])} href="secundaria.html">
15 <span class="material-symbols-outlined">scrollable_header</span>
16 Página secundaria
17 </a>
18
19 <a ${resaltaSiEstasEn(["/iconos.html"])} href="iconos.html">
20 <span class="material-symbols-outlined">sentiment_satisfied</span>
21 Íconos
22 </a>
23
24 <a ${resaltaSiEstasEn(["/botones.html"])} href="botones.html">
25 <span class="material-symbols-outlined">right_click</span>
26 Botones
27 </a>
28
29 <a ${resaltaSiEstasEn(["/campos.html"])} href="campos.html">
30 <span class="material-symbols-outlined">password</span>
31 Campos de texto
32 </a>
33
34 <a ${resaltaSiEstasEn(["/select.html"])} href="select.html">
35 <span class="material-symbols-outlined">bottom_panel_close</span>
36 Select
37 </a>
38
39 <a ${resaltaSiEstasEn(["/interruptor.html"])} href="interruptor.html">
40 <span class="material-symbols-outlined">toggle_on</span>
41 Interruptores
42 </a>
43
44 <a ${resaltaSiEstasEn(["/slider.html"])} href="slider.html">
45 <span class="material-symbols-outlined">linear_scale</span>
46 Sliders
47 </a>
48
49 <a ${resaltaSiEstasEn(["/segmentado.html"])} href="segmentado.html">
50 <span class="material-symbols-outlined">splitscreen_left</span>
51 Botón segmentado
52 </a>
53
54 <a ${resaltaSiEstasEn(["/one-line.html"])} href="one-line.html">
55 <span class="material-symbols-outlined">list</span>
56 Listas one-line
57 </a>
58
59 <a ${resaltaSiEstasEn(["/two-line.html"])} href="two-line.html">
60 <span class="material-symbols-outlined">lists</span>
61 Listas two-line
62 </a>
63
64 <a ${resaltaSiEstasEn(["/three-line.html"])} href="three-line.html">
65 <span class="material-symbols-outlined">receipt_long</span>
66 Listas three-line
67 </a>
68
69 <a ${resaltaSiEstasEn(["/tarjetas.html"])} href="tarjetas.html">
70 <span class="material-symbols-outlined">cards</span>
71 Tarjetas
72 </a>
73
74 <a ${resaltaSiEstasEn(["/navtab.html"])} href="navtab.html">
75 <span class="material-symbols-outlined">swipe_left</span>
76 Pestañas scrollable
77 </a>
78
79 <a ${resaltaSiEstasEn(["/navTabFixed.html"])} href="navTabFixed.html">
80 <span class="material-symbols-outlined">tabs</span>
81 Pestañas fijas
82 </a>
83
84 <a ${resaltaSiEstasEn(["/navbar.html"])} href="navbar.html">
85 <span class="material-symbols-outlined">bottom_navigation</span>
86 Barra de navegación
87 </a>
88
89 <a ${resaltaSiEstasEn(["/formulario.html"])} href="formulario.html">
90 <span class="material-symbols-outlined">newspaper</span>
91 Formulario
92 </a>
93
94 <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html">
95 <span class="material-symbols-outlined">help</span>
96 Ayuda
97 </a>`
98
99 }
100
101}
102
103customElements.define("nav-tab-scrollable", NavTabScrollable)

25. site.webmanifest

  • Este archivo sirve para configurar los instaladores de la aplicación.

Explicación de las propiedades

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}

26. instruccionesListadoSw.txt

1Generar el listado de archivos del sw.js desde Visual Studio Code.
21. Abrir una terminal desde el menú
3 Terminal > New Terminal
4
52. Desde la terminal introducir la orden:
6 Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
83. Abrir el archivo generado, que se llama
9 archivos.txt
10 y sobre este, realizar los pasos que siguen:
11
124. Quita del archivo archivos.txt:
13 * el encabezado,
14 * todas las carpetas,
15 * el archivo .vscode/settings.json,
16 * el archivo archivos.txt,
17 * este archivo (instruccionesListadoSw.txt),
18 * el archivo jsconfig.json,
19 * el archivo sw.js,
20 * el archivo de la base de datos, que termina en ".bd" y
21 está en la carpeta srv,
22 * todos los archivos de php y
23 * las líneas en blanco del final
24
255. Cambia los \ por / desde Visual Studio Code con las siguientes
26 combinaciones de teclas:
27
28 Ctrl+H En el diálogo que aparece introduce lo siguiente:
29 Find:\
30 Replace:/
31
32 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
33
346. Coloca las comillas y coma del final de cada línea desde Visual
35 Studio Code con las siguientes combinaciones de teclas:
36
37 Ctrl+H En el diálogo que aparece, selecciona el botón
38 ".*"
39 e introduce lo siguiente:
40 Find:\s*$
41 Replace:",
42
43 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
44
457. Marca la carpeta inicial, presiona la combinación de teclas:
46
47 Shift+Ctrl+L
48
49 borra la selección, teclea " y luego ESC
50
518. Cambia las secuencias de espacios por / con las siguientes
52 combinaciones de teclas:
53
54 Ctrl+H En el diálogo que aparece, selecciona el botón
55 ".*"
56 e introduce lo siguiente:
57 Find:\s+
58 Replace:/
59
60 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
61
629. Cambia las "/ por " con las siguientes combinaciones de teclas:
63
64 Ctrl+H En el diálogo que aparece, quita la selección del botón
65 ".*"
66 e introduce lo siguiente:
67 Find:"/
68 Replace:"
69
70 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
71
7210. Copia el texto al archivo
73 sw.js
74 en el contenido del arreglo llamado ARCHIVOS, pero recuerda
75 mantener el último elemento, que dice:
76 "/"

27. archivos.txt

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/estilos.css",
22"css/tokens.css",
23"img/Escultura_de_coyote.jpeg",
24"img/icono2048.png",
25"img/maskable_icon.png",
26"img/maskable_icon_x128.png",
27"img/maskable_icon_x192.png",
28"img/maskable_icon_x384.png",
29"img/maskable_icon_x48.png",
30"img/maskable_icon_x512.png",
31"img/maskable_icon_x72.png",
32"img/maskable_icon_x96.png",
33"img/pexels-craig-dennis-3701822.jpg",
34"img/pexels-creative-workshop-3978352.jpg",
35"img/pexels-erik-karits-3732453.jpg",
36"img/pexels-esteban-arango-10226903.jpg",
37"img/pexels-moises-patrício-10961948.jpg",
38"img/pexels-ralph-2270848.jpg",
39"img/pexels-rasmus-svinding-35435.jpg",
40"img/pexels-steve-397857.jpg",
41"img/pexels-vadim-b-141496.jpg",
42"img/screenshot_horizontal.png",
43"img/screenshot_vertical.png",
44"js/configura.js",
45"js/nav-bar.js",
46"js/nav-drw.js",
47"js/nav-tab-fixed.js",
48"js/nav-tab-scrollable.js",
49"lib/css/colors.module.css",
50"lib/css/elevation.css",
51"lib/css/material-symbols-outlined.css",
52"lib/css/md-cards.css",
53"lib/css/md-fab-primary.css",
54"lib/css/md-filled-button.css",
55"lib/css/md-list.css",
56"lib/css/md-menu.css",
57"lib/css/md-navigation-bar.css",
58"lib/css/md-outline-button.css",
59"lib/css/md-ripple.css",
60"lib/css/md-segmented-button.css",
61"lib/css/md-slider-field.css",
62"lib/css/md-standard-icon-button.css",
63"lib/css/md-switch.css",
64"lib/css/md-tab.css",
65"lib/css/md-top-app-bar.css",
66"lib/css/motion.css",
67"lib/css/roboto.css",
68"lib/css/shape.css",
69"lib/css/state.css",
70"lib/css/theme.dark.css",
71"lib/css/theme.light.css",
72"lib/css/typography.css",
73"lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
74"lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
75"lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
76"lib/fonts/roboto-v30-latin-regular.ttf",
77"lib/fonts/roboto-v30-latin-regular.woff2",
78"lib/js/abreElementoHtml.js",
79"lib/js/cierraElementoHtmo.js",
80"lib/js/getAttribute.js",
81"lib/js/htmlentities.js",
82"lib/js/muestraError.js",
83"lib/js/muestraTextoDeAyuda.js",
84"lib/js/ProblemDetails.js",
85"lib/js/querySelector.js",
86"lib/js/querySelectorAll.js",
87"lib/js/registraServiceWorkerSiEsSoportado.js",
88"lib/js/resaltaSiEstasEn.js",
89"lib/js/const/ES_APPLE.js",
90"lib/js/custom/md-filled-text-field.js",
91"lib/js/custom/md-menu-button.js",
92"lib/js/custom/md-options-menu.js",
93"lib/js/custom/md-overflow-button.js",
94"lib/js/custom/md-overflow-menu.js",
95"lib/js/custom/md-select-menu.js",
96"lib/js/custom/md-slider-field.js",
97"lib/js/custom/md-top-app-bar.js",
98"lib/js/custom/MdNavigationDrawer.js",
99"ungap/custom-elements.js",

28. sw.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 */
18const VERSION = "1.00"
19
20/**
21 * Nombre de la carpeta de caché.
22 */
23const CACHE = "pwamd"
24
25/**
26 * Archivos requeridos para que la aplicación funcione fuera de
27 * línea.
28 */
29const 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/estilos.css",
51 "css/tokens.css",
52 "img/Escultura_de_coyote.jpeg",
53 "img/icono2048.png",
54 "img/maskable_icon.png",
55 "img/maskable_icon_x128.png",
56 "img/maskable_icon_x192.png",
57 "img/maskable_icon_x384.png",
58 "img/maskable_icon_x48.png",
59 "img/maskable_icon_x512.png",
60 "img/maskable_icon_x72.png",
61 "img/maskable_icon_x96.png",
62 "img/pexels-craig-dennis-3701822.jpg",
63 "img/pexels-creative-workshop-3978352.jpg",
64 "img/pexels-erik-karits-3732453.jpg",
65 "img/pexels-esteban-arango-10226903.jpg",
66 "img/pexels-moises-patrício-10961948.jpg",
67 "img/pexels-ralph-2270848.jpg",
68 "img/pexels-rasmus-svinding-35435.jpg",
69 "img/pexels-steve-397857.jpg",
70 "img/pexels-vadim-b-141496.jpg",
71 "img/screenshot_horizontal.png",
72 "img/screenshot_vertical.png",
73 "js/configura.js",
74 "js/nav-bar.js",
75 "js/nav-drw.js",
76 "js/nav-tab-fixed.js",
77 "js/nav-tab-scrollable.js",
78 "lib/css/colors.module.css",
79 "lib/css/elevation.css",
80 "lib/css/material-symbols-outlined.css",
81 "lib/css/md-cards.css",
82 "lib/css/md-fab-primary.css",
83 "lib/css/md-filled-button.css",
84 "lib/css/md-list.css",
85 "lib/css/md-menu.css",
86 "lib/css/md-navigation-bar.css",
87 "lib/css/md-outline-button.css",
88 "lib/css/md-ripple.css",
89 "lib/css/md-segmented-button.css",
90 "lib/css/md-slider-field.css",
91 "lib/css/md-standard-icon-button.css",
92 "lib/css/md-switch.css",
93 "lib/css/md-tab.css",
94 "lib/css/md-top-app-bar.css",
95 "lib/css/motion.css",
96 "lib/css/roboto.css",
97 "lib/css/shape.css",
98 "lib/css/state.css",
99 "lib/css/theme.dark.css",
100 "lib/css/theme.light.css",
101 "lib/css/typography.css",
102 "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints",
103 "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf",
104 "lib/fonts/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2",
105 "lib/fonts/roboto-v30-latin-regular.ttf",
106 "lib/fonts/roboto-v30-latin-regular.woff2",
107 "lib/js/abreElementoHtml.js",
108 "lib/js/cierraElementoHtmo.js",
109 "lib/js/getAttribute.js",
110 "lib/js/htmlentities.js",
111 "lib/js/muestraError.js",
112 "lib/js/muestraTextoDeAyuda.js",
113 "lib/js/ProblemDetails.js",
114 "lib/js/querySelector.js",
115 "lib/js/querySelectorAll.js",
116 "lib/js/registraServiceWorkerSiEsSoportado.js",
117 "lib/js/resaltaSiEstasEn.js",
118 "lib/js/const/ES_APPLE.js",
119 "lib/js/custom/md-filled-text-field.js",
120 "lib/js/custom/md-menu-button.js",
121 "lib/js/custom/md-options-menu.js",
122 "lib/js/custom/md-overflow-button.js",
123 "lib/js/custom/md-overflow-menu.js",
124 "lib/js/custom/md-select-menu.js",
125 "lib/js/custom/md-slider-field.js",
126 "lib/js/custom/md-top-app-bar.js",
127 "lib/js/custom/MdNavigationDrawer.js",
128 "ungap/custom-elements.js",
129 "/"
130]
131
132// Verifica si el código corre dentro de un service worker.
133if (self instanceof ServiceWorkerGlobalScope) {
134 // Evento al empezar a instalar el servide worker,
135 self.addEventListener("install",
136 (/** @type {ExtendableEvent} */ evt) => {
137 console.log("El service worker se está instalando.")
138 evt.waitUntil(llenaElCache())
139 })
140
141 // Evento al solicitar información a la red.
142 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
143 if (evt.request.method === "GET") {
144 evt.respondWith(buscaLaRespuestaEnElCache(evt))
145 }
146 })
147
148 // Evento cuando el service worker se vuelve activo.
149 self.addEventListener("activate",
150 () => console.log("El service worker está activo."))
151}
152
153async function llenaElCache() {
154 console.log("Intentando cargar caché:", CACHE, ".")
155 // Borra todos los cachés.
156 const keys = await caches.keys()
157 for (const key of keys) {
158 await caches.delete(key)
159 }
160 // Abre el caché de este service worker.
161 const cache = await caches.open(CACHE)
162 // Carga el listado de ARCHIVOS.
163 await cache.addAll(ARCHIVOS)
164 console.log("Cache cargado:", CACHE, ".")
165 console.log("Versión:", VERSION, ".")
166}
167
168/** @param {FetchEvent} evt */
169async function buscaLaRespuestaEnElCache(evt) {
170 // Abre el caché.
171 const cache = await caches.open(CACHE)
172 const request = evt.request
173 /* Busca la respuesta a la solicitud en el contenido del caché, sin
174 * tomar en cuenta la parte después del símbolo "?" en la URL. */
175 const response = await cache.match(request, { ignoreSearch: true })
176 if (response === undefined) {
177 /* Si no la encuentra, empieza a descargar de la red y devuelve
178 * la promesa. */
179 return fetch(request)
180 } else {
181 // Si la encuentra, devuelve la respuesta encontrada en el caché.
182 return response
183 }
184}

29. Carpeta « css »

A. css / estilos.css

1/* Temas de Material Design 3 */
2@import url(./tokens.css);
3@import url(../lib/css/shape.css);
4@import url(../lib/css/motion.css);
5@import url(../lib/css/state.css);
6@import url(../lib/css/elevation.css);
7@import url(../lib/css/colors.module.css);
8@import url(../lib/css/typography.css);
9@import url(../lib/css/theme.light.css) (prefers-color-scheme: light);
10@import url(../lib/css/theme.dark.css) (prefers-color-scheme: dark);
11/* Fonts utilizados */
12@import url(../lib/css/roboto.css);
13@import url(../lib/css/material-symbols-outlined.css);
14/* CSS de elementos utilizados */
15@import url(../lib/css/md-ripple.css);
16@import url(../lib/css/md-top-app-bar.css);
17@import url(../lib/css/md-menu.css);
18@import url(../lib/css/md-standard-icon-button.css);
19@import url(../lib/css/md-fab-primary.css);
20@import url(../lib/css/md-filled-button.css);
21@import url(../lib/css/md-filled-text-field.css);
22@import url(../lib/css/md-outline-button.css);
23@import url(../lib/css/md-switch.css);
24@import url(../lib/css/md-slider-field.css);
25@import url(../lib/css/md-segmented-button.css);
26@import url(../lib/css/md-list.css);
27@import url(../lib/css/md-cards.css);
28@import url(../lib/css/md-tab.css);
29@import url(../lib/css/md-navigation-bar.css);
30
31html {
32 --tabWidth: 60px;
33 --anchoNav: 360px;
34}
35
36main {
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
47body {
48 margin: 0;
49 display: none;
50 font-family: var(--md-sys-typescale-body-large-font);
51 font-weight: var(--md-sys-typescale-body-large-weight);
52 font-size: var(--md-sys-typescale-body-large-size);
53 font-style: var(--md-sys-typescale-body-large-font-style);
54 letter-spacing: var(--md-sys-typescale-body-large-tracking);
55 line-height: var(--md-sys-typescale-body-large-line-height);
56 text-transform: var(--md-sys-typescale-body-large-text-transform);
57 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
58 color: var(--md-sys-color-on-background);
59 background-color: var(--md-sys-color-background);
60}
61
62body.fouc {
63 display: block;
64 opacity: 0;
65 animation-name: fouc;
66 animation-fill-mode: forwards;
67 animation-duration: 1s;
68}
69
70html {
71 --Font: -apple-system, BlinkMacSystemFont, Roboto, sans-serif;
72 font-size: 16px;
73 --colIntIos: white;
74 --colIntIosOnBk: #2acc2a;
75 --colIntIosOnBkFc: #1bbb1b;
76 --colIntIosOffBk: #dbdbdb;
77 --colIntIosOffBkFc: #BDBDBD;
78 --md-sys-typescale-label-large-weight-prominent:
79 var(--md-ref-typeface-weight-bold);
80 --md-box_shadow_level4:
81 0 var(--md-sys-elevation-level4) var(--md-sys-elevation-level4) var(--md-sys-color-shadow);
82 --md-box_shadow_level3:
83 0 var(--md-sys-elevation-level3) var(--md-sys-elevation-level3) var(--md-sys-color-shadow);
84 --md-box_shadow_level2:
85 0 var(--md-sys-elevation-level2) var(--md-sys-elevation-level2) var(--md-sys-color-shadow);
86 --md-box_shadow_level1:
87 0 var(--md-sys-elevation-level1) var(--md-sys-elevation-level1) var(--md-sys-color-shadow);
88 --md-box_shadow_level0: none;
89 --iconSize: 24px;
90 --avatarSize: 40px;
91 --imageSize: 56px;
92 --videoWidth: 114px;
93 --videoHeight: 64px;
94 --md-sys-state-focus-indicator-outer-offset: 2px;
95 --md-sys-state-focus-indicator-thickness: 3px;
96 /* Pressed state layer opacity */
97 --state-pressed-transparency-percentage: 84%;
98 /* Focus state layer opacity */
99 --state-focus-transparency-percentage: 88%;
100 /* Hover state layer opacity */
101 --state-hover-transparency-percentage: 92%;
102 /* label - small */
103 --md-sys-typescale-label-small-font-family-name: var(--Font);
104 /* label - medium */
105 --md-sys-typescale-label-medium-font-family-name: var(--Font);
106 /* label - large */
107 --md-sys-typescale-label-large-font-family-name: var(--Font);
108 /* body - small */
109 --md-sys-typescale-body-small-font-family-name: var(--Font);
110 /* body - medium */
111 --md-sys-typescale-body-medium-font-family-name: var(--Font);
112 /* body - large */
113 --md-sys-typescale-body-large-font-family-name: var(--Font);
114 /* headline - small */
115 --md-sys-typescale-headline-small-font-family-name: var(--Font);
116 /* headline - medium */
117 --md-sys-typescale-headline-medium-font-family-name: var(--Font);
118 /* headline - large */
119 --md-sys-typescale-headline-large-font-family-name: var(--Font);
120 /* display - small */
121 --md-sys-typescale-display-small-font-family-name: var(--Font);
122 /* display - medium */
123 --md-sys-typescale-display-medium-font-family-name: var(--Font);
124 /* display - large */
125 --md-sys-typescale-display-large-font-family-name: var(--Font);
126 /* title - small */
127 --md-sys-typescale-title-small-font-family-name: var(--Font);
128 /* title - medium */
129 --md-sys-typescale-title-medium-font-family-name: var(--Font);
130 /* title - large */
131 --md-sys-typescale-title-large-font-family-name: var(--Font);
132}
133
134p {
135 margin: 16px;
136}
137
138a {
139 color: var(--md-sys-color-on-background);
140}
141
142@media (prefers-color-scheme: light) {
143 html {
144 --md-riple-color: #00000020;
145 --md-sys-color-surface-container-low: var(--md-ref-palette-neutral95);
146 --md-sys-color-surface-container-highest: var(--md-ref-palette-neutral90);
147 }
148}
149
150@media (prefers-color-scheme: dark) {
151 html {
152 --md-riple-color: #ffffff40;
153 --md-sys-color-surface-container-low: var(--md-ref-palette-neutral20);
154 --md-sys-color-surface-container-highest: var(--md-ref-palette-neutral30);
155 }
156}
157
158@keyframes fouc {
159 from {
160 opacity: 0
161 }
162
163 to {
164 opacity: 1
165 }
166}

B. css / tokens.css

1:root {
2 --md-source: #6750A4;
3 /* error */
4 --md-ref-palette-error0: #000000;
5 --md-ref-palette-error10: #410E0B;
6 --md-ref-palette-error20: #601410;
7 --md-ref-palette-error30: #8C1D18;
8 --md-ref-palette-error40: #B3261E;
9 --md-ref-palette-error50: #DC362E;
10 --md-ref-palette-error60: #E46962;
11 --md-ref-palette-error70: #EC928E;
12 --md-ref-palette-error80: #F2B8B5;
13 --md-ref-palette-error90: #F9DEDC;
14 --md-ref-palette-error95: #FCEEEE;
15 --md-ref-palette-error99: #FFFBF9;
16 --md-ref-palette-error100: #FFFFFF;
17 /* primary */
18 --md-ref-palette-primary0: #000000;
19 --md-ref-palette-primary10: #21005D;
20 --md-ref-palette-primary20: #381E72;
21 --md-ref-palette-primary30: #4F378B;
22 --md-ref-palette-primary40: #6750A4;
23 --md-ref-palette-primary50: #7F67BE;
24 --md-ref-palette-primary60: #9A82DB;
25 --md-ref-palette-primary70: #B69DF8;
26 --md-ref-palette-primary80: #D0BCFF;
27 --md-ref-palette-primary90: #EADDFF;
28 --md-ref-palette-primary95: #F6EDFF;
29 --md-ref-palette-primary99: #FFFBFE;
30 --md-ref-palette-primary100: #FFFFFF;
31 /* secondary */
32 --md-ref-palette-secondary0: #000000;
33 --md-ref-palette-secondary10: #1D192B;
34 --md-ref-palette-secondary20: #332D41;
35 --md-ref-palette-secondary30: #4A4458;
36 --md-ref-palette-secondary40: #625B71;
37 --md-ref-palette-secondary50: #7A7289;
38 --md-ref-palette-secondary60: #958DA5;
39 --md-ref-palette-secondary70: #B0A7C0;
40 --md-ref-palette-secondary80: #CCC2DC;
41 --md-ref-palette-secondary90: #E8DEF8;
42 --md-ref-palette-secondary95: #F6EDFF;
43 --md-ref-palette-secondary99: #FFFBFE;
44 --md-ref-palette-secondary100: #FFFFFF;
45 /* tertiary */
46 --md-ref-palette-tertiary0: #000000;
47 --md-ref-palette-tertiary10: #31111D;
48 --md-ref-palette-tertiary20: #492532;
49 --md-ref-palette-tertiary30: #633B48;
50 --md-ref-palette-tertiary40: #7D5260;
51 --md-ref-palette-tertiary50: #986977;
52 --md-ref-palette-tertiary60: #B58392;
53 --md-ref-palette-tertiary70: #D29DAC;
54 --md-ref-palette-tertiary80: #EFB8C8;
55 --md-ref-palette-tertiary90: #FFD8E4;
56 --md-ref-palette-tertiary95: #FFECF1;
57 --md-ref-palette-tertiary99: #FFFBFA;
58 --md-ref-palette-tertiary100: #FFFFFF;
59 /* neutral */
60 --md-ref-palette-neutral0: #000000;
61 --md-ref-palette-neutral10: #1C1B1F;
62 --md-ref-palette-neutral20: #313033;
63 --md-ref-palette-neutral30: #484649;
64 --md-ref-palette-neutral40: #605D62;
65 --md-ref-palette-neutral50: #787579;
66 --md-ref-palette-neutral60: #939094;
67 --md-ref-palette-neutral70: #AEAAAE;
68 --md-ref-palette-neutral80: #C9C5CA;
69 --md-ref-palette-neutral90: #E6E1E5;
70 --md-ref-palette-neutral95: #F4EFF4;
71 --md-ref-palette-neutral99: #FFFBFE;
72 --md-ref-palette-neutral100: #FFFFFF;
73 --md-ref-palette-neutralNaN: #FFFFFF;
74 /* neutral-variant */
75 --md-ref-palette-neutral-variant0: #000000;
76 --md-ref-palette-neutral-variant10: #1D1A22;
77 --md-ref-palette-neutral-variant20: #322F37;
78 --md-ref-palette-neutral-variant30: #49454F;
79 --md-ref-palette-neutral-variant40: #605D66;
80 --md-ref-palette-neutral-variant50: #79747E;
81 --md-ref-palette-neutral-variant60: #938F99;
82 --md-ref-palette-neutral-variant70: #AEA9B4;
83 --md-ref-palette-neutral-variant80: #CAC4D0;
84 --md-ref-palette-neutral-variant90: #E7E0EC;
85 --md-ref-palette-neutral-variant95: #F5EEFA;
86 --md-ref-palette-neutral-variant99: #FFFBFE;
87 --md-ref-palette-neutral-variant100: #FFFFFF;
88 /* light */
89 --md-sys-color-surface-tint-light: #6750A4;
90 --md-sys-color-surface-tint-color-light: #6750A4;
91 --md-sys-color-on-error-container-light: #410E0B;
92 --md-sys-color-on-error-light: #FFFFFF;
93 --md-sys-color-error-container-light: #F9DEDC;
94 --md-sys-color-on-tertiary-container-light: #31111D;
95 --md-sys-color-on-tertiary-light: #FFFFFF;
96 --md-sys-color-tertiary-container-light: #FFD8E4;
97 --md-sys-color-tertiary-light: #7D5260;
98 --md-sys-color-shadow-light: #000000;
99 --md-sys-color-error-light: #B3261E;
100 --md-sys-color-outline-light: #79747E;
101 --md-sys-color-on-background-light: #1C1B1F;
102 --md-sys-color-background-light: #FFFBFE;
103 --md-sys-color-inverse-on-surface-light: #F4EFF4;
104 --md-sys-color-inverse-surface-light: #313033;
105 --md-sys-color-on-surface-variant-light: #49454F;
106 --md-sys-color-on-surface-light: #1C1B1F;
107 --md-sys-color-surface-variant-light: #E7E0EC;
108 --md-sys-color-surface-light: #FFFBFE;
109 --md-sys-color-on-secondary-container-light: #1D192B;
110 --md-sys-color-on-secondary-light: #FFFFFF;
111 --md-sys-color-secondary-container-light: #E8DEF8;
112 --md-sys-color-secondary-light: #625B71;
113 --md-sys-color-inverse-primary-light: #D0BCFF;
114 --md-sys-color-on-primary-container-light: #21005D;
115 --md-sys-color-on-primary-light: #FFFFFF;
116 --md-sys-color-primary-container-light: #EADDFF;
117 --md-sys-color-primary-light: #6750A4;
118 /* dark */
119 --md-sys-color-surface-tint-dark: #D0BCFF;
120 --md-sys-color-surface-tint-color-dark: #D0BCFF;
121 --md-sys-color-on-error-container-dark: #F2B8B5;
122 --md-sys-color-on-error-dark: #601410;
123 --md-sys-color-error-container-dark: #8C1D18;
124 --md-sys-color-on-tertiary-container-dark: #FFD8E4;
125 --md-sys-color-on-tertiary-dark: #492532;
126 --md-sys-color-tertiary-container-dark: #633B48;
127 --md-sys-color-tertiary-dark: #EFB8C8;
128 --md-sys-color-shadow-dark: #000000;
129 --md-sys-color-error-dark: #F2B8B5;
130 --md-sys-color-outline-dark: #938F99;
131 --md-sys-color-on-background-dark: #E6E1E5;
132 --md-sys-color-background-dark: #1C1B1F;
133 --md-sys-color-inverse-on-surface-dark: #313033;
134 --md-sys-color-inverse-surface-dark: #E6E1E5;
135 --md-sys-color-on-surface-variant-dark: #CAC4D0;
136 --md-sys-color-on-surface-dark: #E6E1E5;
137 --md-sys-color-surface-variant-dark: #49454F;
138 --md-sys-color-surface-dark: #1C1B1F;
139 --md-sys-color-on-secondary-container-dark: #E8DEF8;
140 --md-sys-color-on-secondary-dark: #332D41;
141 --md-sys-color-secondary-container-dark: #4A4458;
142 --md-sys-color-secondary-dark: #CCC2DC;
143 --md-sys-color-inverse-primary-dark: #6750A4;
144 --md-sys-color-on-primary-container-dark: #EADDFF;
145 --md-sys-color-on-primary-dark: #381E72;
146 --md-sys-color-primary-container-dark: #4F378B;
147 --md-sys-color-primary-dark: #D0BCFF;
148 /* label - small */
149 --md-sys-typescale-label-small-font-family-name: Roboto;
150 --md-sys-typescale-label-small-font-family-style: Medium;
151 --md-sys-typescale-label-small-font-weight: 500px;
152 --md-sys-typescale-label-small-font-size: 11px;
153 --md-sys-typescale-label-small-letter-spacing: 0.50px;
154 --md-sys-typescale-label-small-line-height: 16px;
155 /* label - medium */
156 --md-sys-typescale-label-medium-font-family-name: Roboto;
157 --md-sys-typescale-label-medium-font-family-style: Medium;
158 --md-sys-typescale-label-medium-font-weight: 500px;
159 --md-sys-typescale-label-medium-font-size: 12px;
160 --md-sys-typescale-label-medium-letter-spacing: 0.50px;
161 --md-sys-typescale-label-medium-line-height: 16px;
162 /* label - large */
163 --md-sys-typescale-label-large-font-family-name: Roboto;
164 --md-sys-typescale-label-large-font-family-style: Medium;
165 --md-sys-typescale-label-large-font-weight: 500px;
166 --md-sys-typescale-label-large-font-size: 14px;
167 --md-sys-typescale-label-large-letter-spacing: 0.10px;
168 --md-sys-typescale-label-large-line-height: 20px;
169 /* body - small */
170 --md-sys-typescale-body-small-font-family-name: Roboto;
171 --md-sys-typescale-body-small-font-family-style: Regular;
172 --md-sys-typescale-body-small-font-weight: 400px;
173 --md-sys-typescale-body-small-font-size: 12px;
174 --md-sys-typescale-body-small-letter-spacing: 0.40px;
175 --md-sys-typescale-body-small-line-height: 16px;
176 /* body - medium */
177 --md-sys-typescale-body-medium-font-family-name: Roboto;
178 --md-sys-typescale-body-medium-font-family-style: Regular;
179 --md-sys-typescale-body-medium-font-weight: 400px;
180 --md-sys-typescale-body-medium-font-size: 14px;
181 --md-sys-typescale-body-medium-letter-spacing: 0.25px;
182 --md-sys-typescale-body-medium-line-height: 20px;
183 /* body - large */
184 --md-sys-typescale-body-large-font-family-name: Roboto;
185 --md-sys-typescale-body-large-font-family-style: Regular;
186 --md-sys-typescale-body-large-font-weight: 400px;
187 --md-sys-typescale-body-large-font-size: 16px;
188 --md-sys-typescale-body-large-letter-spacing: 0.50px;
189 --md-sys-typescale-body-large-line-height: 24px;
190 /* headline - small */
191 --md-sys-typescale-headline-small-font-family-name: Roboto;
192 --md-sys-typescale-headline-small-font-family-style: Regular;
193 --md-sys-typescale-headline-small-font-weight: 400px;
194 --md-sys-typescale-headline-small-font-size: 24px;
195 --md-sys-typescale-headline-small-letter-spacing: 0px;
196 --md-sys-typescale-headline-small-line-height: 32px;
197 /* headline - medium */
198 --md-sys-typescale-headline-medium-font-family-name: Roboto;
199 --md-sys-typescale-headline-medium-font-family-style: Regular;
200 --md-sys-typescale-headline-medium-font-weight: 400px;
201 --md-sys-typescale-headline-medium-font-size: 28px;
202 --md-sys-typescale-headline-medium-letter-spacing: 0px;
203 --md-sys-typescale-headline-medium-line-height: 36px;
204 /* headline - large */
205 --md-sys-typescale-headline-large-font-family-name: Roboto;
206 --md-sys-typescale-headline-large-font-family-style: Regular;
207 --md-sys-typescale-headline-large-font-weight: 400px;
208 --md-sys-typescale-headline-large-font-size: 32px;
209 --md-sys-typescale-headline-large-letter-spacing: 0px;
210 --md-sys-typescale-headline-large-line-height: 40px;
211 /* display - small */
212 --md-sys-typescale-display-small-font-family-name: Roboto;
213 --md-sys-typescale-display-small-font-family-style: Regular;
214 --md-sys-typescale-display-small-font-weight: 400px;
215 --md-sys-typescale-display-small-font-size: 36px;
216 --md-sys-typescale-display-small-letter-spacing: 0px;
217 --md-sys-typescale-display-small-line-height: 44px;
218 /* display - medium */
219 --md-sys-typescale-display-medium-font-family-name: Roboto;
220 --md-sys-typescale-display-medium-font-family-style: Regular;
221 --md-sys-typescale-display-medium-font-weight: 400px;
222 --md-sys-typescale-display-medium-font-size: 45px;
223 --md-sys-typescale-display-medium-letter-spacing: 0px;
224 --md-sys-typescale-display-medium-line-height: 52px;
225 /* display - large */
226 --md-sys-typescale-display-large-font-family-name: Roboto;
227 --md-sys-typescale-display-large-font-family-style: Regular;
228 --md-sys-typescale-display-large-font-weight: 400px;
229 --md-sys-typescale-display-large-font-size: 57px;
230 --md-sys-typescale-display-large-letter-spacing: -0.25px;
231 --md-sys-typescale-display-large-line-height: 64px;
232 /* title - small */
233 --md-sys-typescale-title-small-font-family-name: Roboto;
234 --md-sys-typescale-title-small-font-family-style: Medium;
235 --md-sys-typescale-title-small-font-weight: 500px;
236 --md-sys-typescale-title-small-font-size: 14px;
237 --md-sys-typescale-title-small-letter-spacing: 0.10px;
238 --md-sys-typescale-title-small-line-height: 20px;
239 /* title - medium */
240 --md-sys-typescale-title-medium-font-family-name: Roboto;
241 --md-sys-typescale-title-medium-font-family-style: Medium;
242 --md-sys-typescale-title-medium-font-weight: 500px;
243 --md-sys-typescale-title-medium-font-size: 16px;
244 --md-sys-typescale-title-medium-letter-spacing: 0.15px;
245 --md-sys-typescale-title-medium-line-height: 24px;
246 /* title - large */
247 --md-sys-typescale-title-large-font-family-name: Roboto;
248 --md-sys-typescale-title-large-font-family-style: Regular;
249 --md-sys-typescale-title-large-font-weight: 400px;
250 --md-sys-typescale-title-large-font-size: 22px;
251 --md-sys-typescale-title-large-letter-spacing: 0px;
252 --md-sys-typescale-title-large-line-height: 28px;
253}
254

30. favicon.ico

  • 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.

favicon.ico

31. Carpeta « img »

A. img / Escultura_de_coyote.jpeg

Coyote de Neza

B. img / icono2048.png

icono2048.png

C. img / maskable_icon.png

maskable_icon.png

D. img / maskable_icon_x128.png

maskable_icon_x128.png

E. img / maskable_icon_x192.png

maskable_icon_x192.png

F. img / maskable_icon_x384.png

maskable_icon_x384.png

G. img / maskable_icon_x48.png

maskable_icon_x48.png

H. img / maskable_icon_x512.png

maskable_icon_x512.png

I. img / maskable_icon_x72.png

maskable_icon_x72.png

J. img / maskable_icon_x96.png

maskable_icon_x96.png

K. img / pexels-craig-dennis-3701822.jpg

Ciudad de San Francisco

L. img / pexels-creative-workshop-3978352.jpg

Perro

M. img / pexels-erik-karits-3732453.jpg

Buho

N. img / pexels-esteban-arango-10226903.jpg

Coyote

O. img / pexels-moises-patrício-10961948.jpg

Avatar de Ana

P. img / pexels-ralph-2270848.jpg

León

Q. img / pexels-rasmus-svinding-35435.jpg

Oso

R. img / pexels-steve-397857.jpg

Lobo

S. img / pexels-vadim-b-141496.jpg

Gato

T. img / screenshot_horizontal.png

screenshot_horizontal.png

U. img / screenshot_vertical.png

screenshot_vertical.png

32. Carpeta « lib »

A. Carpeta « lib / css »

1. lib / css / colors.module.css

1.surface-tint {
2 background-color: var(--md-sys-color-surface-tint);
3}
4.surface-tint-text {
5 color: var(--md-sys-color-surface-tint);
6}
7.surface-tint-color {
8 background-color: var(--md-sys-color-surface-tint-color);
9}
10.surface-tint-color-text {
11 color: var(--md-sys-color-surface-tint-color);
12}
13.on-error-container {
14 background-color: var(--md-sys-color-on-error-container);
15}
16.on-error-container-text {
17 color: var(--md-sys-color-on-error-container);
18}
19.on-error {
20 background-color: var(--md-sys-color-on-error);
21}
22.on-error-text {
23 color: var(--md-sys-color-on-error);
24}
25.error-container {
26 background-color: var(--md-sys-color-error-container);
27}
28.error-container-text {
29 color: var(--md-sys-color-error-container);
30}
31.on-tertiary-container {
32 background-color: var(--md-sys-color-on-tertiary-container);
33}
34.on-tertiary-container-text {
35 color: var(--md-sys-color-on-tertiary-container);
36}
37.on-tertiary {
38 background-color: var(--md-sys-color-on-tertiary);
39}
40.on-tertiary-text {
41 color: var(--md-sys-color-on-tertiary);
42}
43.tertiary-container {
44 background-color: var(--md-sys-color-tertiary-container);
45}
46.tertiary-container-text {
47 color: var(--md-sys-color-tertiary-container);
48}
49.tertiary {
50 background-color: var(--md-sys-color-tertiary);
51}
52.tertiary-text {
53 color: var(--md-sys-color-tertiary);
54}
55.shadow {
56 background-color: var(--md-sys-color-shadow);
57}
58.shadow-text {
59 color: var(--md-sys-color-shadow);
60}
61.error {
62 background-color: var(--md-sys-color-error);
63}
64.error-text {
65 color: var(--md-sys-color-error);
66}
67.outline {
68 background-color: var(--md-sys-color-outline);
69}
70.outline-text {
71 color: var(--md-sys-color-outline);
72}
73.on-background {
74 background-color: var(--md-sys-color-on-background);
75}
76.on-background-text {
77 color: var(--md-sys-color-on-background);
78}
79.background {
80 background-color: var(--md-sys-color-background);
81}
82.background-text {
83 color: var(--md-sys-color-background);
84}
85.inverse-on-surface {
86 background-color: var(--md-sys-color-inverse-on-surface);
87}
88.inverse-on-surface-text {
89 color: var(--md-sys-color-inverse-on-surface);
90}
91.inverse-surface {
92 background-color: var(--md-sys-color-inverse-surface);
93}
94.inverse-surface-text {
95 color: var(--md-sys-color-inverse-surface);
96}
97.on-surface-variant {
98 background-color: var(--md-sys-color-on-surface-variant);
99}
100.on-surface-variant-text {
101 color: var(--md-sys-color-on-surface-variant);
102}
103.on-surface {
104 background-color: var(--md-sys-color-on-surface);
105}
106.on-surface-text {
107 color: var(--md-sys-color-on-surface);
108}
109.surface-variant {
110 background-color: var(--md-sys-color-surface-variant);
111}
112.surface-variant-text {
113 color: var(--md-sys-color-surface-variant);
114}
115.surface {
116 background-color: var(--md-sys-color-surface);
117}
118.surface-text {
119 color: var(--md-sys-color-surface);
120}
121.on-secondary-container {
122 background-color: var(--md-sys-color-on-secondary-container);
123}
124.on-secondary-container-text {
125 color: var(--md-sys-color-on-secondary-container);
126}
127.on-secondary {
128 background-color: var(--md-sys-color-on-secondary);
129}
130.on-secondary-text {
131 color: var(--md-sys-color-on-secondary);
132}
133.secondary-container {
134 background-color: var(--md-sys-color-secondary-container);
135}
136.secondary-container-text {
137 color: var(--md-sys-color-secondary-container);
138}
139.secondary {
140 background-color: var(--md-sys-color-secondary);
141}
142.secondary-text {
143 color: var(--md-sys-color-secondary);
144}
145.inverse-primary {
146 background-color: var(--md-sys-color-inverse-primary);
147}
148.inverse-primary-text {
149 color: var(--md-sys-color-inverse-primary);
150}
151.on-primary-container {
152 background-color: var(--md-sys-color-on-primary-container);
153}
154.on-primary-container-text {
155 color: var(--md-sys-color-on-primary-container);
156}
157.on-primary {
158 background-color: var(--md-sys-color-on-primary);
159}
160.on-primary-text {
161 color: var(--md-sys-color-on-primary);
162}
163.primary-container {
164 background-color: var(--md-sys-color-primary-container);
165}
166.primary-container-text {
167 color: var(--md-sys-color-primary-container);
168}
169.primary {
170 background-color: var(--md-sys-color-primary);
171}
172.primary-text {
173 color: var(--md-sys-color-primary);
174}
175

2. lib / css / elevation.css

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

3. lib / css / material-symbols-outlined.css

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: 24px;
14 width: 24px;
15 height: 24px;
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}

4. lib / css / md-cards.css

1.md-cards {
2 margin: 8px;
3 gap: 8px;
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: 12px;
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: 12px;
84 padding: 0;
85 margin: 0;
86 width: 100%;
87}
88
89.md-cards figure * {
90 border-radius: 12px;
91 width: 100%;
92}
93
94.md-cards .headline {
95 margin: 16px;
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: 16px;
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}

5. lib / css / md-fab-primary.css

1.md-fab-primary {
2 position: relative;
3 display: inline-block;
4 border: none;
5 width: 56px;
6 height: 56px;
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}

6. lib / css / md-filled-button.css

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: 20px;
30 height: 40px;
31 line-height: 40px;
32 padding: 0 24px;
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}

7. lib / css / md-filled-text-field.css

1.md-filled-text-field {
2 position: relative;
3 margin: 16px;
4 overflow: hidden;
5 display: flex;
6 flex-direction: column;
7 align-items: stretch;
8 padding-top: calc(8px + var(--md-sys-typescale-body-small-line-height));
9 border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left);
10 border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right);
11 overflow: hidden;
12}
13
14/* container */
15.md-filled-text-field::before {
16 content: "";
17 position: absolute;
18 z-index: -2;
19 top: 0;
20 right: 0;
21 left: 0;
22 bottom: 0;
23 background-color: var(--md-sys-color-surface-container-highest);
24}
25
26/* state layer */
27.md-filled-text-field::after {
28 content: "";
29 position: absolute;
30 z-index: -1;
31 top: 0;
32 right: 0;
33 left: 0;
34 bottom: 0;
35 background-color: transparent;
36}
37
38.md-filled-text-field span,
39.md-filled-text-field label {
40 position: absolute;
41 top: 8px;
42 left: 16px;
43 right: 16px;
44 display: block;
45 transform: translateY(16px);
46 transition-property: transform;
47 transition-duration: var(--md-sys-motion-duration-300);
48 white-space: nowrap;
49 text-overflow: ellipsis;
50 overflow: hidden;
51 color: var(--md-sys-color-on-surface-variant);
52 font-family: var(--md-sys-typescale-body-large-font);
53 font-weight: var(--md-sys-typescale-body-large-weight);
54 font-size: var(--md-sys-typescale-body-large-size);
55 font-style: var(--md-sys-typescale-body-large-font-style);
56 letter-spacing: var(--md-sys-typescale-body-large-tracking);
57 line-height: var(--md-sys-typescale-body-large-line-height);
58 text-transform: var(--md-sys-typescale-body-large-text-transform);
59 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
60}
61
62.md-filled-text-field :not(:placeholder-shown)+label,
63.md-filled-text-field .populated+span,
64.md-filled-text-field:focus-within span,
65.md-filled-text-field:focus-within label,
66.md-filled-text-field.float span,
67.md-filled-text-field.float label {
68 transform: translateY(0);
69 font-family: var(--md-sys-typescale-body-small-font);
70 font-weight: var(--md-sys-typescale-body-small-weight);
71 font-size: var(--md-sys-typescale-body-small-size);
72 font-style: var(--md-sys-typescale-body-small-font-style);
73 letter-spacing: var(--md-sys-typescale-body-small-tracking);
74 line-height: var(--md-sys-typescale-body-small-line-height);
75 text-transform: var(--md-sys-typescale-body-small-text-transform);
76 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
77}
78
79.md-filled-text-field :not(label, span, small) {
80 position: relative;
81 caret-color: var(--md-sys-color-primary);
82 min-height: 32px;
83 box-sizing: border-box;
84 padding-left: 16px;
85 padding-bottom: 8px;
86 padding-right: 16px;
87 border: none;
88 resize: none;
89 color: var(--md-sys-color-on-surface);
90 font-family: var(--md-sys-typescale-body-large-font);
91 font-weight: var(--md-sys-typescale-body-large-weight);
92 font-size: var(--md-sys-typescale-body-large-size);
93 font-style: var(--md-sys-typescale-body-large-font-style);
94 letter-spacing: var(--md-sys-typescale-body-large-tracking);
95 line-height: var(--md-sys-typescale-body-large-line-height);
96 text-transform: var(--md-sys-typescale-body-large-text-transform);
97 text-decoration: var(--md-sys-typescale-body-large-text-decoration);
98 background-color: transparent;
99 outline: none;
100 border-bottom-width: 1px;
101 border-bottom-style: solid;
102 border-bottom-color: var(--md-sys-color-on-surface-variant);
103}
104
105.md-filled-text-field ::placeholder {
106 color: transparent;
107}
108
109.md-filled-text-field small {
110 display: block;
111 color: var(--md-sys-color-on-surface-variant);
112 background-color: var(--md-sys-color-background);
113 font-family: var(--md-sys-typescale-body-small-font);
114 font-weight: var(--md-sys-typescale-body-small-weight);
115 font-size: var(--md-sys-typescale-body-small-size);
116 font-style: var(--md-sys-typescale-body-small-font-style);
117 letter-spacing: var(--md-sys-typescale-body-small-tracking);
118 line-height: var(--md-sys-typescale-body-small-line-height);
119 text-transform: var(--md-sys-typescale-body-small-text-transform);
120 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
121 padding: 4px 16px 0 16px;
122 white-space: nowrap;
123 text-overflow: ellipsis;
124 overflow: hidden;
125}
126
127.md-filled-text-field:hover span,
128.md-filled-text-field:hover label {
129 color: var(--md-sys-color-on-surface-variant);
130}
131
132.md-filled-text-field:hover :not(label, span, small) {
133 padding-bottom: 8px;
134 border-bottom-width: 1px;
135 border-bottom-color: var(--md-sys-color-on-surface);
136}
137
138.md-filled-text-field:hover::after {
139 background-color: var(--md-sys-color-on-surface);
140 opacity: var(--md-sys-state-hover-state-layer-opacity);
141}
142
143.md-filled-text-field:hover small {
144 color: var(--md-sys-color-on-surface-variant);
145}
146
147.md-filled-text-field:focus-within span,
148.md-filled-text-field:focus-within label {
149 color: var(--md-sys-color-primary);
150}
151
152.md-filled-text-field :focus {
153 color: var(--md-sys-color-on-surface);
154 outline: none;
155 padding-bottom: 7px;
156 border-bottom-width: 2px;
157 border-bottom-color: var(--md-sys-color-primary);
158}
159
160.md-filled-text-field:focus-within small {
161 color: var(--md-sys-color-on-surface-variant);
162}
163
164.md-filled-text-field :invalid {
165 color: var(--md-sys-color-on-surface);
166 border-bottom-color: var(--md-sys-color-error);
167}
168
169.md-filled-text-field :invalid+span,
170.md-filled-text-field :invalid+label {
171 color: var(--md-sys-color-error);
172}
173
174.md-filled-text-field :invalid~small,
175.md-filled-text-field:hover :invalid~small,
176.md-filled-text-field:focus-within .input-text:invalid~small {
177 color: var(--md-sys-color-error);
178}
179
180.md-filled-text-field :invalid:focus {
181 caret-color: var(--md-sys-color-error);
182 border-bottom-color: var(--md-sys-color-on-error-container) !important;
183}
184
185.md-filled-text-field:hover :invalid {
186 color: var(--md-sys-color-on-surface);
187}

8. lib / css / md-list.css

1.md-list {
2 margin: 8px 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: 16px;
46 min-height: 56px;
47 padding: 8px 24px 8px 16px;
48}
49
50.md-list .md-one-line.video,
51.md-list .md-two-line.video {
52 padding: 12px 24px 12px 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: 72px;
63 padding: 8px 24px 8px 16px;
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: 16px;
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: 88px;
113 padding: 12px 24px 12px 16px;
114}
115
116.md-list .md-three-line.video {
117 padding: 12px 24px 12px 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 -webkit-line-clamp: 1;
230}
231
232.md-list .md-three-line .supporting {
233 max-height: calc(2 * var(--md-sys-typescale-body-medium-line-height));
234 -webkit-line-clamp: 2;
235}
236
237.md-list .avatar img,
238.md-list .avatar label,
239.md-list .avatar .material-symbols-outlined:first-child {
240 flex-shrink: 0;
241 background-color: var(--md-sys-color-primary-container);
242 color: var(--md-sys-color-on-primary-container);
243 border-radius: 50%;
244 width: var(--avatarSize);
245 height: var(--avatarSize);
246}
247
248.md-list .avatar label {
249 display: inline-block;
250 font-family: var(--md-sys-typescale-title-medium-font);
251 font-weight: var(--md-sys-typescale-title-medium-weight);
252 font-size: var(--md-sys-typescale-title-medium-size);
253 font-style: var(--md-sys-typescale-title-medium-font-style);
254 letter-spacing: var(--md-sys-typescale-title-medium-tracking);
255 line-height: var(--md-sys-typescale-title-medium-line-height);
256 text-transform: var(--md-sys-typescale-title-medium-text-transform);
257 text-decoration: var(--md-sys-typescale-title-medium-text-decoration);
258 overflow: hidden;
259}
260
261.md-list .avatar .material-symbols-outlined:first-child {
262 font-size: var(--avatarSize);
263}
264
265.md-list .avatar.md-two-line img,
266.md-list .avatar.md-two-line label,
267.md-list .avatar.md-two-line .material-symbols-outlined:first-child {
268 grid-area: img;
269 align-self: center;
270}
271
272.md-list .avatar.md-three-line img,
273.md-list .avatar.md-three-line label,
274.md-list .avatar.md-three-line .material-symbols-outlined:first-child {
275 grid-area: img;
276 align-self: start;
277}
278
279.md-list .icon img,
280.md-list .icon .material-symbols-outlined:first-child {
281 flex-shrink: 0;
282 color: var(--md-sys-color-on-surface-variant);
283 width: var(--iconSize);
284 height: var(--iconSize);
285}
286
287.md-list .icon .material-symbols-outlined:first-child {
288 font-size: var(--iconSize);
289}
290
291.md-list .icon.md-two-line img,
292.md-list .icon.md-two-line .material-symbols-outlined:first-child {
293 grid-area: img;
294 align-self: center;
295}
296
297.md-list .icon.md-three-line img,
298.md-list .icon.md-three-line .material-symbols-outlined:first-child {
299 grid-area: img;
300 align-self: start;
301}
302
303.md-list .video img {
304 flex-shrink: 0;
305 color: var(--md-sys-color-on-surface-variant);
306 width: var(--videoWidth);
307 height: var(--videoHeight);
308}
309
310.md-list .video.md-two-line img {
311 grid-area: img;
312 align-self: center;
313}
314
315.md-list .video.md-three-line img {
316 grid-area: img;
317 align-self: start;
318}
319
320.md-list .image img,
321.md-list .image .material-symbols-outlined:first-child {
322 flex-shrink: 0;
323 color: var(--md-sys-color-on-surface-variant);
324 width: var(--imageSize);
325 height: var(--imageSize);
326}
327
328.md-list .image .material-symbols-outlined:first-child {
329 font-size: var(--imageSize);
330}
331
332.md-list .image.md-two-line img,
333.md-list .image.md-two-line .material-symbols-outlined:first-child {
334 grid-area: img;
335 align-self: center;
336}
337
338.md-list .image.md-three-line img,
339.md-list .image.md-three-line .material-symbols-outlined:first-child {
340 grid-area: img;
341 align-self: start;
342}

9. lib / css / md-menu.css

1.md-menu {
2 display: block;
3 z-index: 2;
4 box-sizing: border-box;
5 cursor: default;
6 padding: 4px 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-property: transform;
13 transition-duration: var(--md-sys-motion-duration-500);
14}
15
16.md-menu[hidden] {
17 display: none
18}
19
20.md-menu.open {
21 transform: translateY(0) scaleY(1);
22}
23
24/* container */
25.md-menu>*::after {
26 content: "";
27 position: absolute;
28 z-index: -2;
29 top: 0;
30 right: 0;
31 left: 0;
32 bottom: 0;
33}
34
35/* container */
36.md-menu>.selected::after {
37 background-color: var(--md-sys-color-secondary-container);
38}
39
40/* label, shape */
41.md-menu>* {
42 position: relative;
43 display: block;
44 box-sizing: border-box;
45 height: 48px;
46 line-height: 48px;
47 padding: 0 12px;
48 color: var(--md-sys-color-on-surface);
49 font-family: var(--md-sys-typescale-label-large-font);
50 font-weight: var(--md-sys-typescale-label-large-weight);
51 font-size: var(--md-sys-typescale-label-large-size);
52 font-style: var(--md-sys-typescale-label-large-font-style);
53 letter-spacing: var(--md-sys-typescale-label-large-tracking);
54 text-transform: var(--md-sys-typescale-label-large-text-transform);
55 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
56 white-space: nowrap;
57 text-overflow: ellipsis;
58 overflow: hidden;
59}
60
61/* label, shape */
62.md-menu>.selected {
63 color: var(--md-sys-color-on-secondary-container);
64}
65
66/* state layer */
67.md-menu>*::before {
68 content: "";
69 position: absolute;
70 z-index: -1;
71 top: 0;
72 right: 0;
73 left: 0;
74 bottom: 0;
75}
76
77/* icon */
78.md-menu>* span {
79 position: relative;
80 margin-right: 12px;
81 vertical-align: middle;
82 color: var(--md-sys-color-on-surface-variant);
83 font-size: 24px;
84 width: 24px;
85 height: 24px;
86}
87
88/* icon */
89.md-menu>.selected span {
90 color: var(--md-sys-color-on-secondary-container);
91}
92
93/* state layer */
94.md-menu>:hover::before {
95 background-color: var(--md-sys-color-on-surface);
96 opacity: var(--md-sys-state-hover-state-layer-opacity);
97}
98
99/* label, shape */
100.md-menu>:hover {
101 color: var(--md-sys-color-on-surface);
102}
103
104/* icon */
105.md-menu>:hover span {
106 color: var(--md-sys-color-on-surface-variant);
107}
108
109/* state layer */
110.md-menu>:focus::before {
111 background-color: var(--md-sys-color-on-surface);
112 opacity: var(--md-sys-state-focus-state-layer-opacity);
113}
114
115/* label, shape */
116.md-menu>:focus {
117 color: var(--md-sys-color-on-surface);
118 outline: none;
119}
120
121/* icon */
122.md-menu>:focus span {
123 color: var(--md-sys-color-on-surface-variant);
124}
125
126/* label, shape */
127.md-menu>:active {
128 background-position: center;
129 background-image:
130 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
131 background-size: 100%;
132 animation-name: md-ripple;
133 animation-duration: var(--md-sys-motion-duration-500);
134 color: var(--md-sys-color-on-surface);
135}
136
137/* state layer */
138.md-menu>:active::before {
139 background-color: var(--md-sys-color-on-surface);
140 opacity: var(--md-sys-state-pressed-state-layer-opacity);
141}
142
143
144/* icon */
145.md-menu>:active span {
146 color: var(--md-sys-color-on-surface-variant);
147}
148
149.md-menu input[type="radio"] {
150 appearance: none;
151 transform: scaleX(0);
152}

10. lib / css / md-navigation-bar.css

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: 12px;
27 padding-bottom: 16px;
28 padding-left: 4px;
29 padding-right: 4px;
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: 32px;
102 width: 64px;
103 line-height: 32px;
104 margin-bottom: 4px;
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: 32px;
127 width: 64px;
128 border-radius: 16px;
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}

11. lib / css / md-outline-button.css

1.md-outline-button {
2 position: relative;
3 box-sizing: border-box;
4 border-radius: 20px;
5 height: 40px;
6 padding: 0 24px;
7 border: 1px 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}

12. lib / css / md-ripple.css

1@keyframes md-ripple {
2
3 from {
4 background-size: 100%;
5 }
6
7 to {
8 background-size: 15000%;
9 }
10
11}

13. lib / css / md-segmented-button.css

1.md-segmented-button {
2 display: flex;
3 align-items: stretch;
4 box-sizing: border-box;
5 border: 1px solid var(--md-sys-color-outline);
6 height: 40px;
7 border-radius: 20px;
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 1px;
19 width: 1px;
20 height: 38px;
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 24px;
41 display: block;
42 box-sizing: border-box;
43 height: 38px;
44 line-height: 38px;
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 12px;
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: 18px;
93 width: 18px;
94 height: 18px;
95 margin-right: 8px;
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}

14. lib / css / md-slider-field.css

1md-slider-field input::-webkit-slider-runnable-track {
2 height: 4px;
3 border-radius: 2px;
4}
5
6md-slider-field input::-webkit-slider-thumb {
7 -webkit-appearance: none;
8 background-color: var(--md-sys-color-primary);
9 width: 20px;
10 height: 20px;
11 border-radius: 10px;
12 margin-top: -9px;
13}
14
15md-slider-field input:hover::-webkit-slider-thumb {
16 box-shadow: 0 0 0 10px color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-hover-transparency-percentage));
17}
18
19md-slider-field input:focus::-webkit-slider-thumb {
20 box-shadow: 0 0 0 10px color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-focus-transparency-percentage));
21}
22
23md-slider-field input:active::-webkit-slider-thumb {
24 box-shadow: 0 0 0 10px 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
33md-slider-field.material::-moz-range-track {
34 height: 4px;
35 border-radius: 2px;
36}
37
38md-slider-field input::-moz-range-thumb {
39 -webkit-appearance: none;
40 appearance: none;
41 background-color: var(--md-sys-color-primary);
42 width: 20px;
43 height: 20px;
44 border: none;
45 border-radius: 10px;
46}
47
48md-slider-field input:hover::-moz-range-thumb {
49 box-shadow: 0 0 0 10px color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-hover-transparency-percentage));
50}
51
52md-slider-field input:focus::-moz-range-thumb {
53 box-shadow: 0 0 0 10px color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-focus-transparency-percentage));
54}
55
56md-slider-field input:active::-moz-range-thumb {
57 box-shadow: 0 0 0 10px 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}

15. lib / css / md-standard-icon-button.css

1.md-standard-icon-button {
2 position: relative;
3 display: inline-block;
4 border: none;
5 padding: 4px;
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: 4px;
21 right: 4px;
22 left: 4px;
23 bottom: 4px;
24 border-radius: 50%;
25}
26
27.md-standard-icon-button span {
28 position: relative;
29 padding: 8px;
30 color: var(--md-sys-color-on-surface-variant);
31 font-size: 24px;
32 width: 24px;
33 height: 24px;
34}
35
36.md-standard-icon-button.avatar span {
37 padding: 5px;
38 font-size: 30px;
39 width: 30px;
40 height: 30px;
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}

16. lib / css / md-switch.css

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 */
18body.material .md-switch {
19 width: 48px;
20 height: 28px;
21 border-radius: 16px;
22 border: 2px solid var(--md-sys-color-outline);
23 background-color: var(--md-sys-color-surface-container-highest);
24}
25
26body.material .md-switch:checked {
27 background-color: var(--md-sys-color-primary);
28}
29
30/* State */
31body.material .md-switch::before {
32 content: "";
33 display: none;
34 position: absolute;
35 height: 40px;
36 width: 40px;
37 border-radius: 20px;
38 top: -6px;
39 left: -6px;
40}
41
42body.material .md-switch:checked:before {
43 left: auto;
44 right: -6px;
45}
46
47/* Handle */
48body.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: 16px;
55 width: 16px;
56 border-radius: 8px;
57 top: 6px;
58 left: 6px;
59 background-color: var(--md-sys-color-outline);
60 box-shadow: var(--md-box_shadow_level1);
61}
62
63body.material .md-switch:checked:after {
64 height: 24px;
65 width: 24px;
66 border-radius: 12px;
67 top: 2px;
68 left: auto;
69 right: 2px;
70 background-color: var(--md-sys-color-on-primary);
71}
72
73body.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
79body.material .md-switch:checked:hover::before {
80 background-color: var(--md-sys-color-primary);
81}
82
83body.material .md-switch:hover::after {
84 background-color: var(--md-sys-color-on-surface-variant);
85}
86
87body.material .md-switch:checked:hover::after {
88 background-color: var(--md-sys-color-primary-container);
89}
90
91body.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
97body.material .md-switch:checked:focus::before {
98 background-color: var(--md-sys-color-primary);
99}
100
101body.material .md-switch:focus::after {
102 background-color: var(--md-sys-color-on-surface-variant);
103}
104
105body.material .md-switch:checked:focus::after {
106 background-color: var(--md-sys-color-primary-container);
107}
108
109body.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
116body.material .md-switch:checked:active::before {
117 background-color: var(--md-sys-color-primary);
118}
119
120body.material .md-switch:active::after {
121 width: 28px;
122 height: 28px;
123 top: 0;
124 left: 0;
125 border-radius: 14px;
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 6px color-mix(in srgb, var(--md-sys-color-on-surface), transparent var(--state-pressed-transparency-percentage)) !important;
134}
135
136body.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 6px color-mix(in srgb, var(--md-sys-color-primary), transparent var(--state-pressed-transparency-percentage)) !important;
143}
144
145body.apple .md-switch {
146 width: 3rem;
147 border-radius: 14px;
148 height: 28px;
149 background-color: var(--colIntIosOffBk);
150}
151
152body.apple .md-switch:checked {
153 background-color: var(--colIntIosOnBk);
154}
155
156body.apple .md-switch:focus {
157 background-color: var(--colIntIosOffBkFc);
158}
159
160body.apple .md-switch:checked:focus {
161 background-color: var(--colIntIosOnBkFc);
162}
163
164body.apple .md-switch::after {
165 content: "";
166 display: inline-block;
167 position: absolute;
168 width: 24px;
169 height: 24px;
170 border-radius: 12px;
171 top: 2px;
172 left: 2px;
173 background-color: var(--colIntIos);
174}
175
176body.apple .md-switch:checked::after {
177 left: auto;
178 right: 2px;
179 border-color: var(--colIntIosOnBk);
180}
181
182body.apple .md-switch:focus::after {
183 border-color: var(--colIntIosOffBkFc);
184}
185
186body.apple .md-switch:checked:focus::after {
187 border-color: var(--colIntIosOnBkFc);
188}

17. lib / css / md-tab.css

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}
9
10.md-tab.fixed {
11 justify-content: center;
12}
13
14.md-tab.scrollable {
15 padding-left: 32px;
16 gap: 16px;
17}
18
19.md-tab.scroll {
20 background-color: var(--md-sys-color-surface-container-low);
21}
22
23.md-tab a {
24 position: relative;
25 display: flex;
26 flex-direction: column;
27 justify-content: start;
28 align-items: center;
29 color: var(--md-sys-color-on-surface-variant);
30 font-family: var(--md-sys-typescale-title-small-font);
31 font-weight: var(--md-sys-typescale-title-small-weight);
32 font-size: var(--md-sys-typescale-title-small-size);
33 font-style: var(--md-sys-typescale-title-small-font-style);
34 letter-spacing: var(--md-sys-typescale-title-small-tracking);
35 line-height: var(--md-sys-typescale-title-small-line-height);
36 text-transform: var(--md-sys-typescale-title-small-text-transform);
37 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
38 text-align: center;
39 box-sizing: border-box;
40 border-bottom: 3px solid var(--md-sys-color-surface);
41}
42
43.md-tab.fixed a {
44 flex: 0 0 var(--tabWidth);
45}
46
47.md-tab.scrollable a {
48 flex: 0 0 auto;
49}
50
51.md-tab a.active {
52 border-bottom-color: var(--md-sys-color-primary);
53}
54
55/* state layer */
56.md-tab a::after {
57 content: "";
58 position: absolute;
59 z-index: -1;
60 top: 0;
61 right: 0;
62 left: 0;
63 bottom: 0;
64 background-color: transparent;
65}
66
67.md-tab span {
68 font-size: var(--iconSize);
69 height: var(--iconSize);
70 width: var(--iconSize);
71 color: var(--md-sys-color-on-surface-variant);
72}
73
74.md-tab .active span {
75 color: var(--md-sys-color-primary);
76}
77
78.md-tab a:hover {
79 color: var(--md-sys-color-on-surface);
80}
81
82/* state layer */
83.md-tab a:hover::after {
84 background-color: var(--md-sys-color-on-surface);
85 opacity: var(--md-sys-state-hover-state-layer-opacity);
86}
87
88.md-tab a.active:hover {
89 color: var(--md-sys-color-primary);
90}
91
92/* state layer */
93.md-tab a.active:hover::after {
94 background-color: var(--md-sys-color-primary);
95 opacity: var(--md-sys-state-hover-state-layer-opacity);
96}
97
98.md-tab a:hover span {
99 color: var(--md-sys-color-on-surface);
100}
101
102.md-tab a.active:hover span {
103 color: var(--md-sys-color-primary);
104}
105
106.md-tab a:focus {
107 outline: none;
108}
109
110/* state layer */
111.md-tab a:focus::after {
112 background-color: var(--md-sys-color-on-surface);
113 opacity: var(--md-sys-state-focus-state-layer-opacity);
114}
115
116/* state layer */
117.md-tab a.active:focus::after {
118 background-color: var(--md-sys-color-primary);
119 opacity: var(--md-sys-state-hover-state-layer-opacity);
120}
121
122.md-tab a:active {
123 background-position: center;
124 background-image:
125 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
126 background-size: 100%;
127 animation-name: md-ripple;
128 animation-duration: var(--md-sys-motion-duration-500);
129}
130
131/* state layer */
132.md-tab a:active::after {
133 background-color: var(--md-sys-color-on-surface);
134 opacity: var(--md-sys-state-pressed-state-layer-opacity);
135}
136
137/* state layer */
138.md-tab a.active:active::after {
139 background-color: var(--md-sys-color-primary);
140 opacity: var(--md-sys-state-pressed-state-layer-opacity);
141}

18. lib / css / md-top-app-bar.css

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-700);
8}
9
10.md-headline.scroll-adicional {
11 color: var(--md-sys-color-surface-container-low);
12 background-color: var(--md-sys-color-surface-container-low);
13}
14
15.md-headline.scroll {
16 color: var(--md-sys-color-surface);
17}
18
19.md-headline.headline-small {
20 padding: 0 16px 24px 16px;
21}
22
23.md-headline.headline-medium {
24 padding: 0 16px 28px 16px;
25}

19. lib / css / motion.css

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

20. lib / css / roboto.css

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+,
11 * Firefox 39+, Safari 12+,
12 * iOS 10+ */
13 src: url('../fonts/roboto-v30-latin-regular.woff2') format('woff2'),
14 /* Chrome 4+, Firefox 3.5+, IE 9+,
15 * Safari 3.1+, iOS 4.2+,
16 * Android Browser 2.2+ */
17 url('../fonts/roboto-v30-latin-regular.ttf') format('truetype');
18}

21. lib / css / shape.css

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

22. lib / css / state.css

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 --md-sys-state-pressed-state-layer-opacity: 0.15999999731779099;
23 /* Focus state layer opacity */
24 --md-sys-state-focus-state-layer-opacity: 0.11999999731779099;
25 /* Hover state layer opacity */
26 --md-sys-state-hover-state-layer-opacity: 0.07999999821186066;
27}
28.hover-state-layer {
29 opacity: var(--md-sys-state-hover-state-layer-opacity);
30}
31.pressed-state-layer {
32 opacity: var(--md-sys-state-pressed-state-layer-opacity);
33}
34.dragged-state-layer {
35 opacity: var(--md-sys-state-dragged-state-layer-opacity);
36}
37.focus-state-layer {
38 opacity: var(--md-sys-state-focus-state-layer-opacity);
39}
40

23. lib / css / theme.dark.css

1:root {
2 --md-sys-color-surface-tint: var(--md-sys-color-surface-tint-dark);
3 --md-sys-color-surface-tint-color: var(--md-sys-color-surface-tint-color-dark);
4 --md-sys-color-on-error-container: var(--md-sys-color-on-error-container-dark);
5 --md-sys-color-on-error: var(--md-sys-color-on-error-dark);
6 --md-sys-color-error-container: var(--md-sys-color-error-container-dark);
7 --md-sys-color-on-tertiary-container: var(--md-sys-color-on-tertiary-container-dark);
8 --md-sys-color-on-tertiary: var(--md-sys-color-on-tertiary-dark);
9 --md-sys-color-tertiary-container: var(--md-sys-color-tertiary-container-dark);
10 --md-sys-color-tertiary: var(--md-sys-color-tertiary-dark);
11 --md-sys-color-shadow: var(--md-sys-color-shadow-dark);
12 --md-sys-color-error: var(--md-sys-color-error-dark);
13 --md-sys-color-outline: var(--md-sys-color-outline-dark);
14 --md-sys-color-on-background: var(--md-sys-color-on-background-dark);
15 --md-sys-color-background: var(--md-sys-color-background-dark);
16 --md-sys-color-inverse-on-surface: var(--md-sys-color-inverse-on-surface-dark);
17 --md-sys-color-inverse-surface: var(--md-sys-color-inverse-surface-dark);
18 --md-sys-color-on-surface-variant: var(--md-sys-color-on-surface-variant-dark);
19 --md-sys-color-on-surface: var(--md-sys-color-on-surface-dark);
20 --md-sys-color-surface-variant: var(--md-sys-color-surface-variant-dark);
21 --md-sys-color-surface: var(--md-sys-color-surface-dark);
22 --md-sys-color-on-secondary-container: var(--md-sys-color-on-secondary-container-dark);
23 --md-sys-color-on-secondary: var(--md-sys-color-on-secondary-dark);
24 --md-sys-color-secondary-container: var(--md-sys-color-secondary-container-dark);
25 --md-sys-color-secondary: var(--md-sys-color-secondary-dark);
26 --md-sys-color-inverse-primary: var(--md-sys-color-inverse-primary-dark);
27 --md-sys-color-on-primary-container: var(--md-sys-color-on-primary-container-dark);
28 --md-sys-color-on-primary: var(--md-sys-color-on-primary-dark);
29 --md-sys-color-primary-container: var(--md-sys-color-primary-container-dark);
30 --md-sys-color-primary: var(--md-sys-color-primary-dark);
31}
32

24. lib / css / theme.light.css

1:root {
2 --md-sys-color-surface-tint: var(--md-sys-color-surface-tint-light);
3 --md-sys-color-surface-tint-color: var(--md-sys-color-surface-tint-color-light);
4 --md-sys-color-on-error-container: var(--md-sys-color-on-error-container-light);
5 --md-sys-color-on-error: var(--md-sys-color-on-error-light);
6 --md-sys-color-error-container: var(--md-sys-color-error-container-light);
7 --md-sys-color-on-tertiary-container: var(--md-sys-color-on-tertiary-container-light);
8 --md-sys-color-on-tertiary: var(--md-sys-color-on-tertiary-light);
9 --md-sys-color-tertiary-container: var(--md-sys-color-tertiary-container-light);
10 --md-sys-color-tertiary: var(--md-sys-color-tertiary-light);
11 --md-sys-color-shadow: var(--md-sys-color-shadow-light);
12 --md-sys-color-error: var(--md-sys-color-error-light);
13 --md-sys-color-outline: var(--md-sys-color-outline-light);
14 --md-sys-color-on-background: var(--md-sys-color-on-background-light);
15 --md-sys-color-background: var(--md-sys-color-background-light);
16 --md-sys-color-inverse-on-surface: var(--md-sys-color-inverse-on-surface-light);
17 --md-sys-color-inverse-surface: var(--md-sys-color-inverse-surface-light);
18 --md-sys-color-on-surface-variant: var(--md-sys-color-on-surface-variant-light);
19 --md-sys-color-on-surface: var(--md-sys-color-on-surface-light);
20 --md-sys-color-surface-variant: var(--md-sys-color-surface-variant-light);
21 --md-sys-color-surface: var(--md-sys-color-surface-light);
22 --md-sys-color-on-secondary-container: var(--md-sys-color-on-secondary-container-light);
23 --md-sys-color-on-secondary: var(--md-sys-color-on-secondary-light);
24 --md-sys-color-secondary-container: var(--md-sys-color-secondary-container-light);
25 --md-sys-color-secondary: var(--md-sys-color-secondary-light);
26 --md-sys-color-inverse-primary: var(--md-sys-color-inverse-primary-light);
27 --md-sys-color-on-primary-container: var(--md-sys-color-on-primary-container-light);
28 --md-sys-color-on-primary: var(--md-sys-color-on-primary-light);
29 --md-sys-color-primary-container: var(--md-sys-color-primary-container-light);
30 --md-sys-color-primary: var(--md-sys-color-primary-light);
31}
32

25. lib / css / typography.css

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: var(--Font);
352 /* Brand typeface */
353 --md-ref-typeface-brand: var(--Font);
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

B. Carpeta « lib / fonts »

1. lib / fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].codepoints

2. lib / fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf

3. lib / fonts / MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2

4. lib / fonts / roboto-v30-latin-regular.ttf

5. lib / fonts / roboto-v30-latin-regular.woff2

C. Carpeta « lib / js »

1. lib / js / abreElementoHtml.js

1/**
2 * @param { HTMLElement } elementoHtml
3 */
4export function abreElementoHtml(elementoHtml) {
5 const list = elementoHtml.classList
6 if (!list.contains("open")) {
7 elementoHtml.hidden = false
8 setTimeout(() => list.add("open"), 100)
9 }
10}

2. lib / js / cierraElementoHtmo.js

1/**
2 * @param { HTMLElement } elementoHtml
3 */
4export function cierraElementoHtmo(elementoHtml) {
5 const list = elementoHtml.classList
6 if (list.contains("open")) {
7 list.remove("open")
8 setTimeout(() => elementoHtml.hidden = true, 500)
9 }
10}
11

3. lib / js / getAttribute.js

1/**
2 * @param {HTMLElement} elementoHtml
3 * @param {string} nombre
4 * @returns {string}
5 */
6export function getAttribute(elementoHtml, nombre) {
7 const valor = elementoHtml.getAttribute(nombre)
8 return valor === null ? "" : valor
9}

4. lib / js / htmlentities.js

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. */
9export 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}

5. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

6. lib / js / muestraTextoDeAyuda.js

1/**
2 * Si un elemento HTML tiene un mensaje de validación, lo
3 * muestra en su elemento de ayuda; en caso contrario, muestra
4 * un mensaje de ayuda.
5 * @param { {
6 * validity: { valid: boolean };
7 * validationMessage: string
8 * } } elementoHtml elemento que contiene datos de validación.
9 * @param { HTMLElement } elementoDeAyuda elemento fonde
10 * se muestran los elementos de validación para elementoHtml.
11 * @param { string } mensajeDeAyuda mensaje de ayuda cuando el estado de
12 * elementoHtml es válido.
13 */
14export function muestraTextoDeAyuda(elementoHtml, elementoDeAyuda,
15 mensajeDeAyuda) {
16 if (elementoHtml.validity.valid) {
17 elementoDeAyuda.textContent = mensajeDeAyuda
18 } else {
19 elementoDeAyuda.textContent = elementoHtml.validationMessage
20 }
21}
22
23// Permite que los eventos de html usen la función.
24window["muestraTextoDeAyuda"] = muestraTextoDeAyuda

7. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

8. lib / js / querySelector.js

1/**
2 * @template { HTMLElement } T
3 * @param { Document | Element | ShadowRoot } raiz
4 * @param { string } query
5 * @returns { T }
6 */
7export 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}

9. lib / js / querySelectorAll.js

1/**
2 * @template { HTMLElement } T
3 * @param { Document | Element | ShadowRoot } raiz
4 * @param { string } query
5 * @returns { T[] }
6 */
7export function querySelectorAll(raiz, query) {
8 return Array.from(raiz.querySelectorAll(query))
9}

10. lib / js / registraServiceWorkerSiEsSoportado.js

1import { muestraError } from "./muestraError.js"
2
3/**
4 * @param { string | URL } urlDeServiceWorker
5 */
6export async function registraServiceWorkerSiEsSoportado(urlDeServiceWorker) {
7 try {
8 if ("serviceWorker" in navigator) {
9 const registro = await navigator.serviceWorker.register(urlDeServiceWorker)
10 console.log(urlDeServiceWorker, "registrado.")
11 console.log(registro)
12 }
13 } catch (error) {
14 muestraError(error)
15 }
16}

11. lib / js / resaltaSiEstasEn.js

1
2/**
3 * @param {string[]} paginas
4 */
5export function resaltaSiEstasEn(paginas) {
6 const pathname = location.pathname
7 return paginas.includes(pathname) ? `class="active"` : ""
8}

12. Carpeta « lib / js / const »

A. lib / js / const / ES_APPLE.js

1export const ES_APPLE = /.*(iPad|iPhone|iPod|Mac).*/.test(navigator.userAgent)

13. Carpeta « lib / js / custom »

A. lib / js / custom / md-menu-button.js

1export class MdMenuButton extends HTMLButtonElement {
2
3 connectedCallback() {
4 this.type = "button"
5 this.classList.add("md-standard-icon-button")
6 this.innerHTML = /* HTML */
7 `<span class="material-symbols-outlined">menu</span>`
8 }
9
10}
11
12customElements.define("md-menu-button", MdMenuButton, { extends: "button" })

B. lib / js / custom / md-options-menu.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3import { querySelector } from "../querySelector.js"
4
5export 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.hidden = true
43 this.role = "listbox"
44 }
45
46 /**
47 * @returns {readonly Readonly<HTMLElement>[]}
48 */
49 get opciones() {
50 return this._opciones
51 }
52
53 get seleccion() {
54 /** @type { HTMLInputElement | null } */
55 const seleccionado = this.querySelector(".selected")
56 return seleccionado === null ? "" : seleccionado.value
57 }
58
59 _configuraOpciones() {
60 /**
61 * @type {HTMLElement[]}
62 */
63 const opciones = []
64 for (const opcion of this._slot.assignedElements()) {
65 opcion.role = "option"
66 if (opcion instanceof HTMLElement) {
67 opciones.push(opcion)
68 }
69 }
70 this._opciones = opciones
71 }
72
73 abre() {
74 abreElementoHtml(this)
75 }
76
77
78 cierra() {
79 cierraElementoHtmo(this)
80 }
81
82 /**
83 * @param {string} value
84 */
85 muestraValue(value) {
86 let texto = ""
87 for (const opcion of this._opciones) {
88 if (opcion.dataset.value === value) {
89 opcion.classList.add("selected")
90 let textContent = opcion.textContent
91 if (texto === "" && textContent !== null) {
92 textContent = textContent.trim()
93 if (textContent !== "") {
94 texto = textContent
95 }
96 }
97 } else {
98 opcion.classList.remove("selected")
99 }
100 }
101 return texto
102 }
103
104}
105
106customElements.define("md-options-menu", MdOptionsMenu)

C. lib / js / custom / md-overflow-button.js

1import { ES_APPLE } from "../const/ES_APPLE.js"
2
3export 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
23customElements
24 .define("md-overflow-button", MdOverflowButton, { extends: "button" })

D. lib / js / custom / md-overflow-menu.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3
4export class MdOverflowMenu extends HTMLElement {
5
6 getContent() {
7 return /* HTML */`
8
9 <style>
10
11 :host {
12 position: fixed;
13 min-width: 112px;
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.hidden = true
44 this.role = "menu"
45 }
46
47 /**
48 * @param {HTMLButtonElement} toggleButton
49 */
50 alterna(toggleButton) {
51 this._toggleButton = toggleButton
52 const top = toggleButton.offsetTop + toggleButton.offsetHeight - 4
53 const right =
54 innerWidth - (toggleButton.offsetLeft + toggleButton.offsetWidth) - 3
55 this.style.top = `${top}px`
56 this.style.right = `${right}px`
57 const list = this.classList
58 if (list.contains("open")) {
59 this.cierra()
60 } else {
61 this.abre()
62 }
63 }
64
65 abre() {
66 document.addEventListener("click", this.clicCierra)
67 abreElementoHtml(this)
68 }
69
70 cierra() {
71 document.removeEventListener("click", this.clicCierra)
72 cierraElementoHtmo(this)
73 }
74
75 /**
76 * @param {Event} evt
77 */
78 clicCierra(evt) {
79 const target = evt.target
80 if (this.classList.contains("open")
81 && this._toggleButton !== null
82 && target instanceof HTMLElement
83 && !this._toggleButton.contains(target)) {
84 this.cierra()
85 }
86 }
87}
88
89customElements.define("md-overflow-menu", MdOverflowMenu)

E. lib / js / custom / md-select-menu.js

1import { getAttribute } from "../getAttribute.js"
2import { querySelector } from "../querySelector.js"
3import { MdOptionsMenu } from "./md-options-menu.js"
4
5export 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: 32px;
24 white-space: nowrap;
25 text-overflow: ellipsis;
26 overflow: hidden;
27 }
28
29 #up {
30 position: absolute;
31 bottom: 8px;
32 right: 12px;
33 display: none;
34 color: var(--md-sys-color-on-surface-variant);
35 }
36
37 #down {
38 position: absolute;
39 bottom: 8px;
40 right: 12px;
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
422MdSelectMenu.formAssociated = true
423
424customElements.define("md-select-menu", MdSelectMenu)

F. lib / js / custom / md-slider-field.js

1import { querySelector } from "../querySelector.js"
2
3export class MdSliderField extends HTMLElement {
4
5 getContent() {
6 return /* HTML */`
7 <style>
8 :host {
9 display: block;
10 margin: 16px;
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: 4px;
37 border-radius: 2px;
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: 8px;
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
120customElements.define("md-slider-field", MdSliderField)

G. lib / js / custom / md-top-app-bar.js

1import { ES_APPLE } from "../const/ES_APPLE.js"
2import { getAttribute } from "../getAttribute.js"
3import { querySelector } from "../querySelector.js"
4
5class 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 4px;
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, 64px);
22 width: env(titlebar-area-width, 100%);
23 }
24
25 :host(.apple) {
26 height: env(titlebar-area-height, 48px);
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 48px;
51 overflow: hidden
52 }
53
54 #headline::slotted(*) {
55 -webkit-app-region: drag;
56 white-space: nowrap;
57 text-overflow: ellipsis;
58 overflow: hidden;
59 font-family: var(--md-sys-typescale-title-large-font);
60 font-weight: var(--md-sys-typescale-title-large-weight);
61 font-size: var(--md-sys-typescale-title-large-size);
62 font-style: var(--md-sys-typescale-title-large-font-style);
63 letter-spacing: var(--md-sys-typescale-title-large-tracking);
64 line-height: var(--md-sys-typescale-title-large-line-height);
65 text-transform: var(--md-sys-typescale-title-large-text-transform);
66 text-decoration: var(--md-sys-typescale-title-large-text-decoration);
67 color: var(--md-sys-color-on-surface);
68 transition-property: opacity;
69 transition-duration: var(--md-sys-motion-duration-700);
70 }
71
72 :host(.headline) #headline::slotted(*) {
73 opacity: 0;
74 }
75
76 :host(.scroll.headline) #headline::slotted(*) {
77 opacity: 1;
78 }
79
80 :host(.center-aligned) #headline::slotted(*) {
81 flex: 1 1 auto;
82 text-align: center
83 }
84
85 </style>
86
87 <span id="navigation">
88 <slot name="navigation"></slot>
89 </span>
90 <slot id="headline"></slot>
91 <span id="acciones">
92 <slot name="action"></slot>
93 </span>`
94 }
95
96 constructor() {
97 super()
98 if (ES_APPLE) {
99 document.body.classList.add("apple")
100 document.body.classList.remove("material")
101 } else {
102 document.body.classList.add("material")
103 document.body.classList.remove("apple")
104 }
105
106 /**
107 * @private
108 * @readonly
109 */
110 const shadow = this.attachShadow({ mode: "open" })
111 shadow.innerHTML = this.getContent()
112 this._configuraAction = this._configuraAction.bind(this)
113 /**
114 * @private
115 * @type {number}
116 */
117 this._posY = 0
118 /**
119 * @private
120 * @type {boolean}
121 */
122 this._scrolling = false
123 /**
124 * @private
125 * @type { HTMLSlotElement }
126 */
127 this._navigation = querySelector(shadow, '[name="navigation"]')
128 /**
129 * @private
130 * @type { HTMLSlotElement }
131 */
132 this._action = querySelector(shadow, '[name="action"]')
133 /**
134 * @private
135 * @type { HTMLHeadingElement | null }
136 */
137 this._headline = null
138 /**
139 * @private
140 * @type { HTMLElement | null }
141 */
142 this._adicional = null
143 this._action.addEventListener("slotchange", this._configuraAction)
144 addEventListener("scroll", () => this._onScroll())
145 addEventListener("load", () => this.configurOtros())
146 }
147
148 connectedCallback() {
149 this.role = "toolbar"
150 this._configuraAction()
151 }
152
153 configurOtros() {
154 const idHeadline = getAttribute(this, "headline")
155 if (idHeadline !== "") {
156 const headline = document.getElementById(idHeadline)
157 if (headline instanceof HTMLHeadingElement) {
158 this._headline = headline
159 this.classList.add("headline")
160 if (this.classList.contains("apple") || this.classList.contains("medium")) {
161 headline.classList.add("md-headline", "headline-small")
162 } else {
163 headline.classList.add("md-headline", "headline-medium")
164 }
165 }
166 }
167 const idAdicional = getAttribute(this, "adicional")
168 if (idAdicional !== "") {
169 this._adicional = document.getElementById(idAdicional)
170 if (this._adicional !== null) {
171 if (this.classList.contains("apple")) {
172 this._adicional.style.top = "env(titlebar-area-height, 48px)"
173 } else {
174 this._adicional.style.top = "env(titlebar-area-height, 64px)"
175 }
176 }
177 }
178 }
179
180 _configuraAction() {
181 const assignedElements = this._action.assignedElements()
182 if (this.isConnected) {
183 if (ES_APPLE) {
184 this.classList.add("apple")
185 this.classList.remove("material")
186 } else {
187 this.classList.add("material")
188 this.classList.remove("apple")
189 }
190 if (this.classList.contains("center-aligned")) {
191 this.classList.remove("centrado")
192 this.classList.remove("justificado")
193 } else {
194 if (ES_APPLE && assignedElements.length <= 1) {
195 this.classList.add("centrado")
196 this.classList.remove("justificado")
197 } else {
198 this.classList.add("justificado")
199 this.classList.remove("centrado")
200 }
201 }
202 }
203 }
204
205 /** @private */
206 _onScroll() {
207 this._posY = scrollY
208 if (!this._scrolling) {
209 requestAnimationFrame(() => this._avanza())
210 }
211 this._scrolling = true
212 }
213
214 /** @private */
215 _avanza() {
216 if (this._posY === 0) {
217 this.classList.remove("scroll")
218 if (this._headline !== null) {
219 if (this._adicional === null) {
220 this._headline.classList.remove("scroll")
221 } else {
222 this._headline.classList.remove("scroll-adicional")
223 }
224 }
225 if (this._adicional !== null) {
226 this._adicional.classList.remove("scroll")
227 }
228 } else {
229 this.classList.add("scroll")
230 if (this._headline !== null) {
231 if (this._adicional === null) {
232 this._headline.classList.add("scroll")
233 } else {
234 this._headline.classList.add("scroll-adicional")
235 }
236 }
237 if (this._adicional !== null) {
238 this._adicional.classList.add("scroll")
239 }
240 }
241 this._scrolling = false
242 }
243
244}
245
246customElements.define("md-top-app-bar", MdTopAppBar)

H. lib / js / custom / MdNavigationDrawer.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3import { querySelector } from "../querySelector.js"
4
5export 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/shape.css">
17 <link rel="stylesheet" href="/lib/css/md-ripple.css">
18
19 <style>
20
21 nav {
22 display: flex;
23 flex-direction: column;
24 position: fixed;
25 z-index: 4;
26 box-sizing: border-box;
27 top: 0;
28 left: 0;
29 bottom: 0;
30 width: var(--anchoNav);
31 max-width: 80vw;
32 overflow: hidden;
33 transform: translateX(-100%);
34 background-color: var(--md-sys-color-surface-container-low);
35 transition-property: transform;
36 transition-duration: var(--md-sys-motion-duration-700);
37 }
38
39 :host(.open) nav {
40 transform: translateX(0);
41 }
42
43 nav>div {
44 flex-grow: 1;
45 overflow: auto;
46 padding: 12px 16px;
47 }
48
49 h1 {
50 margin: 0;
51 height: 56px;
52 line-height: 56px;
53 padding: 0 0 0 12px;
54 white-space: nowrap;
55 text-overflow: ellipsis;
56 overflow: hidden;
57 color: var(--md-sys-color-on-surface-variant);
58 font-family: var(--md-sys-typescale-title-small-font);
59 font-weight: var(--md-sys-typescale-title-small-weight);
60 font-size: var(--md-sys-typescale-title-small-size);
61 font-style: var(--md-sys-typescale-title-small-font-style);
62 letter-spacing: var(--md-sys-typescale-title-small-tracking);
63 text-transform: var(--md-sys-typescale-title-small-text-transform);
64 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
65 }
66
67 a::after { /* container inactive */
68 content: "";
69 position: absolute;
70 z-index: -2;
71 top: 0;
72 right: 0;
73 left: 0;
74 bottom: 0;
75 }
76
77 a.active::after { /* container */
78 background-color: var(--md-sys-color-secondary-container);
79 }
80
81 a { /* label, shape inactive */
82 position: relative;
83 display: block;
84 box-sizing: border-box;
85 height: 56px;
86 line-height: 56px;
87 padding: 0 12px;
88 border-radius: 28px;
89 color: var(--md-sys-color-on-surface-variant);
90 font-family: var(--md-sys-typescale-label-large-font);
91 font-weight: var(--md-sys-typescale-label-large-weight);
92 font-size: var(--md-sys-typescale-label-large-size);
93 font-style: var(--md-sys-typescale-label-large-font-style);
94 letter-spacing: var(--md-sys-typescale-label-large-tracking);
95 text-transform: var(--md-sys-typescale-label-large-text-transform);
96 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
97 overflow: hidden;
98 white-space: nowrap;
99 text-overflow: ellipsis;
100 }
101
102 a.active { /* label, shape */
103 font-weight: var(--md-sys-typescale-label-large-weight-prominent);
104 color: var(--md-sys-color-on-secondary-container);
105 }
106
107 a::before { /* state layer */
108 content: "";
109 position: absolute;
110 z-index: -1;
111 top: 0;
112 right: 0;
113 left: 0;
114 bottom: 0;
115 }
116
117 a span { /* inactive icon */
118 position: relative;
119 margin-right: 12px;
120 vertical-align: middle;
121 color: var(--md-sys-color-on-surface-variant);
122 font-size: 24px;
123 width: 24px;
124 height: 24px;
125 }
126
127 a.active span { /* icon */
128 color: var(--md-sys-color-on-secondary-container);
129 }
130
131 #scrim {
132 position: fixed;
133 z-index: 3;
134 top: 0;
135 left: 0;
136 bottom: 0;
137 right: 0;
138 background-color: var(--md-ref-palette-neutral-variant20);
139 opacity: 0.4;
140 transform: translateX(-100%);
141 transition-property: transform;
142 animation-duration: var(--md-sys-motion-duration-700);
143 }
144
145 :host(.open) #scrim {
146 transform: translateX(0);
147 }
148
149 a:hover { /* inactive label, shape */
150 color: var(--md-sys-color-on-surface);
151 }
152
153 a.active:hover { /* active label, shape */
154 color: var(--md-sys-color-on-secondary-container);
155 }
156
157 a:hover::before { /* inactive state layer */
158 background-color: var(--md-sys-color-on-surface);
159 opacity: var(--md-sys-state-hover-state-layer-opacity);
160 }
161
162 a.active:hover::before { /* state layer */
163 background-color: var(--md-sys-color-on-secondary-container);
164 }
165
166 a:hover span { /* inactive icon */
167 color: var(--md-sys-color-on-surface);
168 }
169
170 a.active:hover span { /* icon */
171 color: var(--md-sys-color-on-secondary-container);
172 }
173
174 a:focus { /* inactive label, shape */
175 outline: none;
176 color: var(--md-sys-color-on-surface);
177 }
178
179 a.active:focus { /* label, shape */
180 color: var(--md-sys-color-on-secondary-container);
181 }
182
183 a:focus::before { /* inactive state layer */
184 background-color: var(--md-sys-color-on-surface);
185 opacity: var(--md-sys-state-focus-state-layer-opacity);
186 }
187
188 a.active:focus::before { /* state layer */
189 background-color: var(--md-sys-color-on-secondary-container);
190 }
191
192 a:focus span { /* inactive icon */
193 color: var(--md-sys-color-on-surface);
194 }
195
196 a.active:focus span { /* icon */
197 color: var(--md-sys-color-on-secondary-container);
198 }
199
200
201 a:active { /* inactive pressed label, shape */
202 background-position: center;
203 background-image:
204 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
205 background-size: 100%;
206 animation-name: md-ripple;
207 animation-duration: var(--md-sys-motion-duration-500);
208 color: var(--md-sys-color-on-surface);
209 }
210
211 a.active:active { /* active pressed label, shape */
212 color: var(--md-sys-color-on-secondary-container);
213 }
214
215 a:active::before { /* inactive pressed state layer */
216 background-color: var(--md-sys-color-on-surface);
217 opacity: var(--md-sys-state-pressed-state-layer-opacity);
218 }
219
220 a.active:active::before { /* active pressed state layer */
221 background-color: var(--md-sys-color-on-secondary-container);
222 }
223
224 a:active span { /* inactive pressed icon */
225 color: var(--md-sys-color-on-surface);
226 }
227
228 a.active:focus span { /* active pressed icon */
229 color: var(--md-sys-color-on-secondary-container);
230 }
231
232 </style>
233
234 <div id="scrim"></div>
235 <nav class="large-end"><div></div></nav>`
236 }
237
238 constructor() {
239 super()
240 const shadow = this.attachShadow({ mode: "open", delegatesFocus: true })
241 shadow.innerHTML = this.getContent()
242 this.cierra = this.cierra.bind(this)
243
244 /** @type {HTMLUListElement} */
245 this._div = querySelector(shadow, "nav>div")
246
247 /** @type {HTMLUListElement} */
248 this._scrim = querySelector(shadow, "#scrim")
249 this._scrim.addEventListener("click", this.cierra)
250 }
251
252 connectedCallback() {
253 this.hidden = true
254 this._div.innerHTML = this.getHipervinculos()
255 }
256
257 abre() {
258 abreElementoHtml(this)
259 }
260
261 cierra() {
262 cierraElementoHtmo(this)
263 }
264
265}

33. Carpeta « ungap »

A. ungap / custom-elements.js

1/*! (c) Andrea Giammarchi @webreflection ISC */
2!function(){"use strict";var e=function(e,t){var n=function(e){for(var t=0,n=e.length;t<n;t++)r(e[t])},r=function(e){var t=e.target,n=e.attributeName,r=e.oldValue;t.attributeChangedCallback(n,r,t.getAttribute(n))};return function(o,a){var l=o.constructor.observedAttributes;return l&&e(a).then((function(){new t(n).observe(o,{attributes:!0,attributeOldValue:!0,attributeFilter:l});for(var e=0,a=l.length;e<a;e++)o.hasAttribute(l[e])&&r({target:o,attributeName:l[e],oldValue:null})})),o}};function t(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n<t;n++)r[n]=e[n];return r}function n(e,n){var r="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(!r){if(Array.isArray(e)||(r=function(e,n){if(e){if("string"==typeof e)return t(e,n);var r=Object.prototype.toString.call(e).slice(8,-1);return"Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r?Array.from(e):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?t(e,n):void 0}}(e))||n&&e&&"number"==typeof e.length){r&&(e=r);var o=0,a=function(){};return{s:a,n:function(){return o>=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:a}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l,i=!0,c=!1;return{s:function(){r=r.call(e)},n:function(){var e=r.next();return i=e.done,e},e:function(e){c=!0,l=e},f:function(){try{i||null==r.return||r.return()}finally{if(c)throw l}}}}
3/*! (c) Andrea Giammarchi - ISC */var r=!0,o=!1,a="querySelectorAll",l="querySelectorAll",i=self,c=i.document,u=i.Element,s=i.MutationObserver,f=i.Set,d=i.WeakMap,h=function(e){return l in e},v=[].filter,p=function(e){var t=new d,i=function(n,r){var o;if(r)for(var a,l=function(e){return e.matches||e.webkitMatchesSelector||e.msMatchesSelector}(n),i=0,c=y.length;i<c;i++)l.call(n,a=y[i])&&(t.has(n)||t.set(n,new f),(o=t.get(n)).has(a)||(o.add(a),e.handle(n,r,a)));else t.has(n)&&(o=t.get(n),t.delete(n),o.forEach((function(t){e.handle(n,r,t)})))},p=function(e){for(var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],n=0,r=e.length;n<r;n++)i(e[n],t)},y=e.query,g=e.root||c,m=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:document,l=arguments.length>2&&void 0!==arguments[2]?arguments[2]:MutationObserver,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:["*"],c=function t(o,l,i,c,u,s){var f,d=n(o);try{for(d.s();!(f=d.n()).done;){var h=f.value;(s||a in h)&&(u?i.has(h)||(i.add(h),c.delete(h),e(h,u)):c.has(h)||(c.add(h),i.delete(h),e(h,u)),s||t(h[a](l),l,i,c,u,r))}}catch(e){d.e(e)}finally{d.f()}},u=new l((function(e){if(i.length){var t,a=i.join(","),l=new Set,u=new Set,s=n(e);try{for(s.s();!(t=s.n()).done;){var f=t.value,d=f.addedNodes,h=f.removedNodes;c(h,a,l,u,o,o),c(d,a,l,u,r,o)}}catch(e){s.e(e)}finally{s.f()}}})),s=u.observe;return(u.observe=function(e){return s.call(u,e,{subtree:r,childList:r})})(t),u}(i,g,s,y),w=u.prototype.attachShadow;return w&&(u.prototype.attachShadow=function(e){var t=w.call(this,e);return m.observe(t),t}),y.length&&p(g[l](y)),{drop:function(e){for(var n=0,r=e.length;n<r;n++)t.delete(e[n])},flush:function(){for(var e=m.takeRecords(),t=0,n=e.length;t<n;t++)p(v.call(e[t].removedNodes,h),!1),p(v.call(e[t].addedNodes,h),!0)},observer:m,parse:p}},y=self,g=y.document,m=y.Map,w=y.MutationObserver,b=y.Object,E=y.Set,S=y.WeakMap,A=y.Element,M=y.HTMLElement,O=y.Node,N=y.Error,C=y.TypeError,T=y.Reflect,q=b.defineProperty,D=b.keys,I=b.getOwnPropertyNames,P=b.setPrototypeOf,k=!self.customElements,L=function(e){for(var t=D(e),n=[],r=new E,o=t.length,a=0;a<o;a++){n[a]=e[t[a]];try{delete e[t[a]]}catch(e){r.add(a)}}return function(){for(var a=0;a<o;a++)r.has(a)||(e[t[a]]=n[a])}};if(k){var x=function(){var e=this.constructor;if(!$.has(e))throw new C("Illegal constructor");var t=$.get(e);if(W)return F(W,t);var n=H.call(g,t);return F(P(n,e.prototype),t)},H=g.createElement,$=new m,_=new m,j=new m,R=new m,V=[],U=p({query:V,handle:function(e,t,n){var r=j.get(n);if(t&&!r.isPrototypeOf(e)){var o=L(e);W=P(e,r);try{new r.constructor}finally{W=null,o()}}var a="".concat(t?"":"dis","connectedCallback");a in r&&e[a]()}}).parse,W=null,B=function(e){if(!_.has(e)){var t,n=new Promise((function(e){t=e}));_.set(e,{$:n,_:t})}return _.get(e).$},F=e(B,w);self.customElements={define:function(e,t){if(R.has(e))throw new N('the name "'.concat(e,'" has already been used with this registry'));$.set(t,e),j.set(e,t.prototype),R.set(e,t),V.push(e),B(e).then((function(){U(g.querySelectorAll(e))})),_.get(e)._(t)},get:function(e){return R.get(e)},whenDefined:B},q(x.prototype=M.prototype,"constructor",{value:x}),self.HTMLElement=x,g.createElement=function(e,t){var n=t&&t.is,r=n?R.get(n):R.get(e);return r?new r:H.call(g,e)},"isConnected"in O.prototype||q(O.prototype,"isConnected",{configurable:!0,get:function(){return!(this.ownerDocument.compareDocumentPosition(this)&this.DOCUMENT_POSITION_DISCONNECTED)}})}else if(k=!self.customElements.get("extends-br"))try{var z=function e(){return self.Reflect.construct(HTMLBRElement,[],e)};z.prototype=HTMLLIElement.prototype;var G="extends-br";self.customElements.define("extends-br",z,{extends:"br"}),k=g.createElement("br",{is:G}).outerHTML.indexOf(G)<0;var J=self.customElements,K=J.get,Q=J.whenDefined;self.customElements.whenDefined=function(e){var t=this;return Q.call(this,e).then((function(n){return n||K.call(t,e)}))}}catch(e){}if(k){var X=function(e){var t=ae.get(e);ve(t.querySelectorAll(this),e.isConnected)},Y=self.customElements,Z=g.createElement,ee=Y.define,te=Y.get,ne=Y.upgrade,re=T||{construct:function(e){return e.call(this)}},oe=re.construct,ae=new S,le=new E,ie=new m,ce=new m,ue=new m,se=new m,fe=[],de=[],he=function(e){return se.get(e)||te.call(Y,e)},ve=p({query:de,handle:function(e,t,n){var r=ue.get(n);if(t&&!r.isPrototypeOf(e)){var o=L(e);we=P(e,r);try{new r.constructor}finally{we=null,o()}}var a="".concat(t?"":"dis","connectedCallback");a in r&&e[a]()}}).parse,pe=p({query:fe,handle:function(e,t){ae.has(e)&&(t?le.add(e):le.delete(e),de.length&&X.call(de,e))}}).parse,ye=A.prototype.attachShadow;ye&&(A.prototype.attachShadow=function(e){var t=ye.call(this,e);return ae.set(this,t),t});var ge=function(e){if(!ce.has(e)){var t,n=new Promise((function(e){t=e}));ce.set(e,{$:n,_:t})}return ce.get(e).$},me=e(ge,w),we=null;I(self).filter((function(e){return/^HTML.*Element$/.test(e)})).forEach((function(e){var t=self[e];function n(){var e=this.constructor;if(!ie.has(e))throw new C("Illegal constructor");var n=ie.get(e),r=n.is,o=n.tag;if(r){if(we)return me(we,r);var a=Z.call(g,o);return a.setAttribute("is",r),me(P(a,e.prototype),r)}return oe.call(this,t,[],e)}q(n.prototype=t.prototype,"constructor",{value:n}),q(self,e,{value:n})})),g.createElement=function(e,t){var n=t&&t.is;if(n){var r=se.get(n);if(r&&ie.get(r).tag===e)return new r}var o=Z.call(g,e);return n&&o.setAttribute("is",n),o},Y.get=he,Y.whenDefined=ge,Y.upgrade=function(e){var t=e.getAttribute("is");if(t){var n=se.get(t);if(n)return void me(P(e,n.prototype),t)}ne.call(Y,e)},Y.define=function(e,t,n){if(he(e))throw new N("'".concat(e,"' has already been defined as a custom element"));var r,o=n&&n.extends;ie.set(t,o?{is:e,tag:o}:{is:"",tag:e}),o?(r="".concat(o,'[is="').concat(e,'"]'),ue.set(r,t.prototype),se.set(e,t),de.push(r)):(ee.apply(Y,arguments),fe.push(r=e)),ge(e).then((function(){o?(ve(g.querySelectorAll(r)),le.forEach(X,[r])):pe(g.querySelectorAll(r))})),ce.get(e)._(t)}}}();

34. jsconfig.json

  • 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": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

35. Resumen

  • 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.

6. Acceso al dispositivo

Versión para imprimir.

A. Introduccion

  • En esta lección se muestra como aceder directamente a algunas funciones del dispositivo.

B. Geolocalización

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

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>

C. Archivos y cámara

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

Observa como funciona en Android.

Observa como funciona en iOS.

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>

D. Uso directo de la cámara

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

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>

E. Resumen

  • En esta lección se revisaron:

    • Acceso al GPS.

    • Acceso a la cámara.

    • Acceso a los archivos.

7. IoT

Versión para imprimir.

A. Introduccion

B. Diagrama de despliegue

Diagrama de despliegue

C. Hazlo funcionar

  1. Revisa el proyecto iothtml en Replit con la URl https://replit.com/@GilbertoPachec2/iothtml?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Este proyecto puede correr simultáneamente en varios navegadores y computadoras, todos interactuan con el servidor test.mosquitto.org.

  3. Usa o crea una cuenta de Google.

  4. Crea una cuenta de Replit usando la cuenta de Google.

  5. Crea el proyecto iothtml con la categoría HTML, CSS, JS en Replit y edita o sube los archivos de iothtml.

  6. Depura el proyecto.

  7. Crea la cover page o página de spotlight del proyecto.

D. Archivos

E. index.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>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 { creaClientIdMqtt } from "./lib/js/creaClientIdMqtt.js"
34 import { falloEnLaConexionMqtt } from "./lib/js/falloEnLaConexionMqtt.js"
35 import { conexionMqttPerdida } from "./lib/js/conexionMqttPerdida.js"
36 import { muestraError } from "./lib/js/muestraError.js"
37
38 // A cada control le corresponde un tópico diferente.
39 const TOPICO_FOCO = "gilpgdm/IoT/foco"
40
41 let valor = "0"
42
43 // Cambia por una raíz para tu proyecto.
44 const clientId = creaClientIdMqtt("gilpgdmIoT-")
45
46 // Si usas un servidor de MQTT diferente, necesitas cambiar los parámetros.
47 const cliente = new Paho.MQTT.Client("test.mosquitto.org", 8081, clientId)
48
49 function clicEnBoton() {
50 try {
51 enviaMensajeMqtt(valor === '1' ? '0' : '1', TOPICO_FOCO)
52 } catch (error) {
53 muestraError(error)
54 }
55 }
56 // Permite que los eventos de html usen la función.
57 window["clicEnBoton"] = 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 // Permite que los eventos de html usen la función.
104 window["enviaMensajeMqtt"] = enviaMensajeMqtt
105
106 </script>
107
108</body>
109
110</html>

F. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / conexionMqttPerdida.js

1/**
2 * @param { {
3 * errorCode: number,
4 * errorMessage: string
5 * } } responseObject
6 */
7export 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}
14
15// Permite que los eventos de html usen la función.
16window["conexionMqttPerdida"] = conexionMqttPerdida

2. lib / js / creaClientIdMqtt.js

1/**
2 * Añade caracteres al azar a una raíz, para obtener un clientId único por cada
3 * instancia que se conecte al servidor de mqtt.
4 * @param {string} raiz
5 */
6export function creaClientIdMqtt(raiz) {
7 const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
8 for (var i = 0; i < 15; i++) {
9 raiz += chars.charAt(Math.floor(Math.random() * chars.length))
10 }
11 return raiz
12}
13
14// Permite que los eventos de html usen la función.
15window["creaClientIdMqtt"] = creaClientIdMqtt

3. lib / js / falloEnLaConexionMqtt.js

1/**
2 * @param { { errorMessage: string } } res
3 */
4export function falloEnLaConexionMqtt(res) {
5 const mensaje = "Fallo en conexión:" + res.errorMessage
6 console.error(mensaje)
7 alert(mensaje)
8}
9
10// Permite que los eventos de html usen la función.
11window["falloEnLaConexionMqtt"] = falloEnLaConexionMqtt

4. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

5. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

G. Carpeta « paho.javascript-1.0.3 »

A. paho.javascript-1.0.3 / about.html

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
13indicated below, the Content is provided to you under the terms and conditions of the
14Eclipse Public License Version 1.0 ("EPL") and Eclipse Distribution License Version 1.0 ("EDL").
15A 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>
17and 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>.
19For 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
22being redistributed by another party ("Redistributor") and different terms and conditions may
23apply to your use of any object code in the Content. Check the Redistributor's license that was
24provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
25indicated below, the terms and conditions of the EPL still apply to any source code in the Content
26and such source code may be obtained at <a href="http://www.eclipse.org/">http://www.eclipse.org</a>.</p>
27
28</body></html>
29

B. paho.javascript-1.0.3 / CONTRIBUTING.md

1# Contributing to Paho
2
3Thanks for your interest in this project!
4
5You can contribute bugfixes and new features by sending pull requests through GitHub.
6
7## Legal
8
9In order for your contribution to be accepted, it must comply with the Eclipse Foundation IP policy.
10
11Please read the [Eclipse Foundation policy on accepting contributions via Git](http://wiki.eclipse.org/Development_Resources/Contributing_via_Git).
12
131. 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.
162. Go to your [account settings](https://dev.eclipse.org/site_login/myaccount.php#open_tab_accountsettings) and add your GitHub username to your account.
173. Make sure that you _sign-off_ your Git commits in the following format:
18 ``` Signed-off-by: John Smith ``` This is usually at the bottom of the commit message. You can automate this by adding the '-s' flag when you make the commits. e.g. ```git commit -s -m "Adding a cool feature"```
194. 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
251. [Fork the repository on GitHub](https://github.com/eclipse/paho.mqtt.javascript/fork)
262. Clone the forked repository onto your computer: ``` git clone https://github.com//paho.mqtt.javascript.git ```
273. Create a new branch from the latest ```develop``` branch with ```git checkout -b YOUR_BRANCH_NAME origin/develop```
284. Make your changes
295. If developing a new feature, make sure to include JUnit tests.
306. Ensure that all new and existing tests pass.
317. Commit the changes into the branch: ``` git commit -s ``` Make sure that your commit message is meaningful and describes your changes correctly.
328. If you have a lot of commits for the change, squash them into a single / few commits.
339. Push the changes in your branch to your forked repository.
3410. 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
37What happens next depends on the content of the patch. If it is 100% authored
38by the contributor and is less than 1000 lines (and meets the needs of the
39project), then it can be pulled into the main repository. If not, more steps
40are 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
48Information 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
52Contact:
53--------
54
55Contact the project developers via the project's development
56[mailing list](https://dev.eclipse.org/mailman/listinfo/paho-dev).
57
58Search for bugs:
59----------------
60
61This 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
63Create a new bug:
64-----------------
65
66Be 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

C. paho.javascript-1.0.3 / edl-v10

1
2Eclipse Distribution License - v 1.0
3
4Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors.
5
6All rights reserved.
7
8Redistribution 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
14THIS 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

D. paho.javascript-1.0.3 / epl-v10

1Eclipse Public License - v 1.0
2
3THE 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
51. DEFINITIONS
6
7"Contribution" means:
8
9a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and
10b) in the case of each subsequent Contributor:
11i) changes to the Program, and
12ii) additions to the Program;
13where 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
222. GRANT OF RIGHTS
23
24a) 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.
25b) 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.
26c) 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.
27d) 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.
283. REQUIREMENTS
29
30A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that:
31
32a) it complies with the terms and conditions of this Agreement; and
33b) its license agreement:
34i) 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;
35ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits;
36iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and
37iv) 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.
38When the Program is made available in source code form:
39
40a) it must be made available under this Agreement; and
41b) a copy of this Agreement must be included with each copy of the Program.
42Contributors may not remove or alter any copyright notices contained within the Program.
43
44Each 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
464. COMMERCIAL DISTRIBUTION
47
48Commercial 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
50For 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
525. NO WARRANTY
53
54EXCEPT 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
566. DISCLAIMER OF LIABILITY
57
58EXCEPT 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
607. GENERAL
61
62If 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
64If 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
66All 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
68Everyone 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
70This 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

E. paho.javascript-1.0.3 / paho-mqtt-min.js

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}
15function 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&
171023));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,
18text:"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}."},
19UNSUPPORTED:{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}."},
20MALFORMED_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="{"+
21g+"}",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)+
222,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;
24break;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),
25f=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(),
26e=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),
27this._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"]));
28if(!("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!==
29g.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=
30null;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",
31b,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);
33this._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)};
34d.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]=
36v(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,
38this);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);
39this.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/
412),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:"+
42this._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++;
43a.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])};
44d.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<
46a.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];
47localStorage.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)&&
48a.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});
49c=!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}),
50this.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);
51if(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();
52break;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});
54this._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=
55function(){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);
56else 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]);
57else 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!==
59this._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=
61g=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};
62this._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=
63a};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=
64a;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===
66a.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]||
690>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.");
70if("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===
71arguments.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");
72if(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=
73function(){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()},
74set 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)},
75get 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=
76function(){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"===
77typeof 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()},
78set 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)});

F. paho.javascript-1.0.3 / paho-mqtt.js

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>
58client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId");
59client.onConnectionLost = onConnectionLost;
60client.onMessageArrived = onMessageArrived;
61client.connect({onSuccess:onConnect});
62
63function 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};
71function onConnectionLost(responseObject) {
72 if (responseObject.errorCode !== 0)
73 console.log("onConnectionLost:"+responseObject.errorMessage);
74};
75function 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
100var 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);
2412return PahoMQTT;
2413});
2414

H. jsconfig.json

  • 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": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

I. Resumen

  • En esta lección se presentó el proyecto base para IoT.

8. ESP32

Versión para imprimir.

A. Introduccion

  • En esta lección se presentan programas para el NODE MCU ESP32 y como interactuar con el sistema de IoT.

B. Software a instalar

Microsoft Edge Chromium
Mozilla Firefox
Google Chrome
CP210x USB to UART Bridge VCP Drivers
Arduino IDE
Arduino core for ESP8266 WiFi chip
Arduino core for ESP32 WiFi chip
EspMQTTClient
  • 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:

    1. Selecciona la pestaña SOFTWARE y busca el DOWNLOAD para tu sistema operativo.

    2. 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
    3. 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.

    4. Instala las tarjetas esp32 y esp8266.

    5. 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.

    6. Instala la libresría EspMQTTClient. Se instala como una librería normal. Si te solicita instalar otras librerías, instálalas también.

C. Blink.ino

Video de la placa

Ve como capturar y ejecutar este ejemplo.

Ve como desplegar este ejemplo en Tinkercad y compartirlo.

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. */
6void 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. */
20void 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

D. Button.ino

Video de la placa

Ve como desplegar este ejemplo en Tinkercad y compartirlo.

1/* Este es un derivado de Button
2 por DojoDave y Tom Igoe */
3
4/* Pin para el botón. */
5const int BOTON = 0;
6
7int estadoDelBoton = 0;
8
9void 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
18void 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

E. Dispositivo.ino

  • 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.

Video del dispositivo
Implementación del ejemplo de IoT con MQTT en Arduino IDE.

Ve como configurar y ejecutar este ejemplo. No hagas caso a la parte de los certificados,

1#include "EspMQTTClient.h"
2
3const char *const SSID =
4 "Galaxy A723C85";
5const char *const PASS =
6 "bdoi1764";
7const char *const URL =
8 "test.mosquitto.org";
9const uint16_t PUERTO =
10 1883;
11const char *const CLIENT_ID =
12 "gilpgdmIoT-esp32-1";
13const char *const TOPICO_FOCO =
14 "gilpgdm/IoT/foco";
15
16EspMQTTClient cliente(
17 SSID,
18 PASS,
19 URL,
20 0, // Usuario opcional
21 0, // Contraseña opcional
22 CLIENT_ID,
23 PUERTO);
24const int BOTON = 0;
25bool presionado = false;
26String valor = "0";
27
28void recibeMensaje(
29 const String &payload)
30{
31 valor = payload;
32 digitalWrite(LED_BUILTIN,
33 valor == "1"
34 ? HIGH
35 : LOW);
36}
37
38void enviaMensajeMqtt(
39 String valorAEnviar,
40 String topico)
41{
42 cliente.publish(topico,
43 valorAEnviar);
44}
45
46void onConnectionEstablished()
47{
48 cliente.subscribe(
49 TOPICO_FOCO, recibeMensaje);
50 enviaMensajeMqtt(
51 valor, TOPICO_FOCO);
52}
53
54void 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
72void 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

F. Resumen

  • En esta lección se presentaron los siguientes archivos:

    • Blinkt.ino

    • Button.ino

    • Dispositivo.ino

9. Simulación de un sistema IoT

Versión para imprimir.

A. Introduccion

  • 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:

    cuca
    Vendedor
    pepito
    Cliente
    juan
    Instalador
    angela
    Operador
    perla
    Cobranza

B. Contratación

  • 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.


Contrato y cita

Usuario: Cuca

C. Instalación del sistema IoT

  • 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.

D. Uso del sistema de 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.

Video del dispositivo
Implementación del ejemplo de IoT con MQTT en Arduino IDE.

Foco

Nombre del cliente

Pepito

Usu del cliente

pepito

Id del dispositivo

169

E. Monitoreo

  • 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.


Monitoreo

  • Botón presionado.

    Id del dispositivo = 169.

    Valor = 0.

    Guardando valor en la base de datos.

F. Almacenamiento en tabla

  • 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..


Tabla de consumo

HISTORICO_ID DISPOSITIVO_ID HISTORICO_VALOR HISTORICO_TIMESTAMP

G. Facturación

  • 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.


Factura de noviembre de 2024

Nombre del cliente

Pepito

Id del dispositivo

169

Tiempo de encendido del foco

3000 minutos

Total a pagar

$600.00

H. Resumen

  • En esta lección ejemplificó el funcionamiento de un sistema completo de IoT

10. Notificaciones

Salida

Ábrelo en otra pestaña.

Revísalo en gilpgedit.

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>

11. Notificaciones push

Versión para imprimir.

1. Introduccion

  • En esta lección se muestra un ejemplo de notificaciones push.

  • Puedes probar la app desde varios navegadores y dispositivos en https://replit.com/@GilbertoPachec2/notificacionespush?v=1. Hazle fork al proyecto y córrelo.

  • Primero suscríbete y luego envía notificaciones a todos los que estén conectados.

2. Diagrama entidad relación

Diagrama entidad relación

3. Diagrama de despliegue

Diagrama de despliegue

4. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec2/notificacionespush?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Usa o crea una cuenta de Google.

  3. Crea una cuenta de Replit usando la cuenta de Google.

  4. Crea un proyecto PHP Web Server en Replit y edita o sube los archivos de este proyecto.

  5. 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.

  6. 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.

  7. El proyecto ya contiene la carpeta vendor y el archivo composer.lock, pero es posible crearlos con estos pasos:

    1. Instalar composer. Si trabajas desde Replit, los archivos .replit y replit.nix de este proyecto lo instalan automáticamente. Para Windows, usa el instalador de https://getcomposer.org/download/.

    2. Para Replit, usa la pestaña shell, que ya está posicionada en la carpeta del proyecto. Si estas en Windows, debes abrir el símbolo del sistema y usar el comando cd para llegar a la carpeta del proyecto.

    3. Ejecuta el comando
      composer update

  8. Depura el proyecto.

  9. Crea la cover page o página de spotlight del proyecto.

5. Archivos

6. index.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>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/generallaves.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 {
55 activaNotificacionesPush
56 } from "./lib/js/activaNotificacionesPush.js"
57 import { getSuscripcionPush } from "./lib/js/getSuscripcionPush.js"
58 import { suscribeAPush } from "./lib/js/suscribeAPush.js"
59 import { cancelaSuscripcionPush } from "./lib/js/cancelaSuscripcionPush.js"
60 import { invocaServicio } from "./lib/js/invocaServicio.js"
61 import { enviaJson } from "./lib/js/enviaJson.js"
62 import { muestraError } from "./lib/js/muestraError.js"
63 import { muestraObjeto } from "./lib/js/muestraObjeto.js"
64 import { urlBase64ToUint8Array } from "./lib/js/urlBase64ToUint8Array.js"
65 import {
66 calculaDtoParaSuscripcion
67 } from "./lib/js/calculaDtoParaSuscripcion.js"
68
69 const applicationServerKey = urlBase64ToUint8Array(
70 "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0")
71 /** @enum {string} */
72 const Estado = {
73 CALCULANDO: "Calculando…",
74 SUSCRITO: "Suscrito",
75 DESUSCRITO: "Sin suscripción",
76 INCOMPATIBLE: "Incompatible"
77 }
78
79 async function preparaVista() {
80 try {
81 await activaNotificacionesPush("sw.js")
82 const suscripcion = await getSuscripcionPush()
83 if (suscripcion === null) {
84 muestraEstado(Estado.DESUSCRITO)
85 } else {
86 // Modifica la suscripción en el servidor,
87 const dto = calculaDtoParaSuscripcion(suscripcion)
88 await enviaJson("srv/srvSuscripcion.php", dto, "PUT")
89 muestraEstado(Estado.SUSCRITO)
90 }
91 } catch (error) {
92 muestraEstado(Estado.INCOMPATIBLE)
93 muestraError(error)
94 }
95 }
96 // Permite que los eventos de html usen la función.
97 window["preparaVista"] = preparaVista
98
99 async function notificaDesdeElServidor() {
100 try {
101 reporte.innerHTML =
102 /* html */ `<progress max="100">Cargando…</progress>`
103 const render = await invocaServicio("srv/srvNotifica.php")
104 await muestraObjeto(document, render.body)
105 } catch (error) {
106 muestraError(error)
107 }
108 }
109 // Permite que los eventos de html usen la función.
110 window["notificaDesdeElServidor"] = notificaDesdeElServidor
111
112 async function suscribe() {
113 try {
114 muestraEstado(Estado.CALCULANDO)
115 const suscripcion = await suscribeAPush(applicationServerKey)
116 // Agrega la suscripción al servidor,
117 const dto = calculaDtoParaSuscripcion(suscripcion)
118 await enviaJson("srv/srvSuscripcion.php", dto, "POST")
119 muestraEstado(Estado.SUSCRITO)
120 } catch (error) {
121 muestraError(error)
122 }
123 }
124 // Permite que los eventos de html usen la función.
125 window["suscribe"] = suscribe
126
127 async function cancela() {
128 try {
129 muestraEstado(Estado.CALCULANDO)
130 const suscripcion = await cancelaSuscripcionPush()
131 if (suscripcion !== null) {
132 // Elimina la suscripción en el servidor,
133 const dto = calculaDtoParaSuscripcion(suscripcion)
134 await enviaJson("srv/srvSuscripcion.php", dto, "DELETE")
135 muestraEstado(Estado.DESUSCRITO)
136 }
137 } catch (error) {
138 muestraError(error)
139 }
140 }
141 // Permite que los eventos de html usen la función.
142 window["cancela"] = cancela
143
144 /** @param {Estado} estado */
145 function muestraEstado(estado) {
146 outEstado.value = estado
147 if (estado === Estado.INCOMPATIBLE || estado === Estado.CALCULANDO) {
148 btnSuscribe.hidden = true
149 btnCancela.hidden = true
150 btnNotifica.hidden = true
151 } else if (estado === Estado.SUSCRITO) {
152 btnSuscribe.hidden = true
153 btnCancela.hidden = false
154 btnNotifica.hidden = false
155 } else if (estado === Estado.DESUSCRITO) {
156 btnSuscribe.hidden = false
157 btnCancela.hidden = true
158 btnNotifica.hidden = true
159 }
160 }
161
162 </script>
163
164</body>
165
166</html>

7. sw.js

1if (self instanceof ServiceWorkerGlobalScope) {
2 // El siguiente código se activa cuando llega una notificación push.
3 self.addEventListener("push", (/** @type {PushEvent} */ evt) => {
4 const notificacion = evt.data
5 /* Si el navegador tiene permitido mostrar notificaciones push,
6 * nuestra la que se ha recibido. */
7 if (notificacion !== null && self.Notification.permission === 'granted') {
8 evt.waitUntil(muestraNotificacion(notificacion))
9 }
10 })
11}
12
13/**
14 * @param {PushMessageData} notificacion
15 */
16async function muestraNotificacion(notificacion) {
17 if (self instanceof ServiceWorkerGlobalScope) {
18 const mensaje = notificacion.text()
19 await self.registration.showNotification(mensaje)
20 }
21}

8. Carpeta « srv »

A. srv / generallaves.php

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>

B. srv / srvNotifica.php

1<?php
2
3require_once __DIR__ . "/../vendor/autoload.php";
4require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
5require_once __DIR__ . "/modelo/Suscripcion.php";
6require_once __DIR__ . "/bd/suscripcionConsulta.php";
7require_once __DIR__ . "/bd/suscripcionElimina.php";
8require_once __DIR__ . "/bd/suscripcionElimina.php";
9
10use Minishlink\WebPush\WebPush;
11
12const AUTH = [
13 "VAPID" => [
14 "subject" => "https://notificacionesphp.gilbertopachec2.repl.co/",
15 "publicKey" => "BMBlr6YznhYMX3NgcWIDRxZXs0sh7tCv7_YCsWcww0ZCv9WGg-tRCXfMEHTiBPCksSqeve1twlbmVAZFv7GSuj0",
16 "privateKey" => "vplfkITvu0cwHqzK9Kj-DYStbCH_9AhGx9LqMyaeI6w"
17 ]
18];
19
20ejecutaServicio(function () {
21
22 $webPush = new WebPush(AUTH);
23 $mensaje = "Hola! 👋";
24
25 // Envia el mensaje a todas las suscripciones.
26 $suscripciones = suscripcionConsulta();
27 foreach ($suscripciones as $suscripcion) {
28 $webPush->queueNotification($suscripcion, $mensaje);
29 }
30 $reportes = $webPush->flush();
31
32 // Genera el reporte de envio a cada suscripcion.
33 $reporteDeEnvios = "";
34 foreach ($reportes as $reporte) {
35 $endpoint = htmlentities($reporte->getRequest()->getUri());
36 if ($reporte->isSuccess()) {
37 // Reporte de éxito.
38 $reporteDeEnvios .= "<dt>$endpoint</dt><dd>Éxito</dd>";
39 } else {
40 if ($reporte->isSubscriptionExpired()) {
41 suscripcionElimina($endpoint);
42 }
43 // Reporte de fallo.
44 $explicacion = htmlentities($reporte->getReason());
45 $reporteDeEnvios .= "<dt>$endpoint</dt><dd>Fallo: $explicacion</dd>";
46 }
47 }
48
49 return ["reporte" => ["innerHTML" => $reporteDeEnvios]];
50});
51

C. srv / srvSuscripcion.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/leeJson.php";
5require_once __DIR__ . "/../lib/php/JsonResponse.php";
6require_once __DIR__ . "/modelo/Suscripcion.php";
7require_once __DIR__ . "/modelo/leeSuscripcion.php";
8require_once __DIR__ . "/bd/suscripcionAgrega.php";
9require_once __DIR__ . "/bd/suscripcionBusca.php";
10require_once __DIR__ . "/bd/suscripcionModifica.php";
11require_once __DIR__ . "/bd/suscripcionElimina.php";
12
13ejecutaServicio(function () {
14
15 $json = leeJson();
16
17 $modelo = leeSuscripcion($json);
18
19 $metodoHttp = $_SERVER["REQUEST_METHOD"];
20
21 if ($metodoHttp === "POST") {
22 suscripcionAgrega($modelo);
23 return new JsonResponse(JsonResponse_Created);
24 } elseif ($metodoHttp === "PUT") {
25 $registrado = suscripcionBusca($modelo->endpoint);
26 if ($registrado) {
27 suscripcionModifica($modelo);
28 return $modelo;
29 } else {
30 suscripcionAgrega($modelo);
31 return new JsonResponse(JsonResponse_Created);
32 }
33 } elseif ($metodoHttp === "DELETE") {
34 suscripcionElimina($modelo->endpoint);
35 return JsonResponse::noContent();
36 }
37
38 return [];
39});
40

D. Carpeta « srv / bd »

1. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/bdCrea.php";
4
5class Bd
6{
7
8 private static ?PDO $conexion = null;
9
10 static function getConexion(): PDO
11 {
12 if (self::$conexion === null) {
13
14 self::$conexion = new PDO(
15 // cadena de conexión
16 "sqlite:notificacionespush.db",
17 // usuario
18 null,
19 // contraseña
20 null,
21 // Opciones: conexiones persistentes y lanza excepciones.
22 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
23 );
24
25 bdCrea(self::$conexion);
26 }
27 return self::$conexion;
28 }
29}
30

2. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS SUSCRIPCION (
7 SUS_ENDPOINT TEXT NOT NULL,
8 SUS_PUB_KEY TEXT NOT NULL,
9 SUS_AUT_TOK TEXT NOT NULL,
10 SUS_CONT_ENCOD TEXT NOT NULL,
11 CONSTRAINT SUS_PK
12 PRIMARY KEY(SUS_ENDPOINT)
13 )'
14 );
15}
16

3. srv / bd / suscripcionAgrega.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/../modelo/Suscripcion.php";
5
6function suscripcionAgrega(Suscripcion $modelo)
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "INSERT INTO SUSCRIPCION
11 (SUS_ENDPOINT, SUS_PUB_KEY, SUS_AUT_TOK, SUS_CONT_ENCOD)
12 VALUES
13 (:endpoint, :publicKey, :authToken, :contentEncoding)"
14 );
15 $stmt->execute([
16 ":endpoint" => $modelo->endpoint,
17 ":publicKey" => $modelo->publicKey,
18 ":authToken" => $modelo->authToken,
19 ":contentEncoding" => $modelo->contentEncoding
20 ]);
21}
22

4. srv / bd / suscripcionBusca.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/../modelo/Suscripcion.php";
5
6function suscripcionBusca(string $endpoint)
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "SELECT
11 SUS_ENDPOINT as endpoint,
12 SUS_PUB_KEY as publicKey,
13 SUS_AUT_TOK as authToken,
14 SUS_CONT_ENCOD as contentEncoding
15 FROM SUSCRIPCION
16 WHERE SUS_ENDPOINT = :endpoint"
17 );
18 $stmt->execute([":endpoint" => $endpoint]);
19 $stmt->setFetchMode(
20 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
21 Suscripcion::class
22 );
23 return $stmt->fetch();
24}
25

5. srv / bd / suscripcionConsulta.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/../modelo/Suscripcion.php";
5
6function suscripcionConsulta()
7{
8 $con = Bd::getConexion();
9 $stmt = $con->query(
10 "SELECT
11 SUS_ENDPOINT as endpoint,
12 SUS_PUB_KEY as publicKey,
13 SUS_AUT_TOK as authToken,
14 SUS_CONT_ENCOD as contentEncoding
15 FROM SUSCRIPCION"
16 );
17 return $stmt->fetchAll(
18 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
19 Suscripcion::class
20 );
21}
22

6. srv / bd / suscripcionElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function suscripcionElimina(string $endpoint)
6{
7 $con = Bd::getConexion();
8 $stmt = $con->prepare(
9 "DELETE FROM SUSCRIPCION
10 WHERE SUS_ENDPOINT = :endpoint"
11 );
12 $stmt->execute([":endpoint" => $endpoint]);
13}
14

7. srv / bd / suscripcionModifica.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/../modelo/Suscripcion.php";
5
6function suscripcionModifica(Suscripcion $modelo)
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "UPDATE SUSCRIPCION
11 SET
12 SUS_PUB_KEY = :publicKey,
13 SUS_AUT_TOK = :authToken,
14 SUS_CONT_ENCOD = :contentEncoding
15 WHERE SUS_ENDPOINT = :endpoint"
16 );
17 $stmt->execute([
18 ":endpoint" => $modelo->endpoint,
19 ":publicKey" => $modelo->publicKey,
20 ":authToken" => $modelo->authToken,
21 ":contentEncoding" => $modelo->contentEncoding
22 ]);
23}
24

E. Carpeta « srv / modelo »

1. srv / modelo / leeSuscripcion.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/Suscripcion.php";
5
6function leeSuscripcion($objeto)
7{
8
9 if (!isset($objeto->endpoint) || !is_string($objeto->endpoint))
10 throw new ProblemDetails(
11 status: ProblemDetails::BadRequest,
12 type: "/error/endpointincorrecto.html",
13 title: "El endpoint debe ser texto.",
14 );
15
16 if (!isset($objeto->publicKey) || !is_string($objeto->publicKey))
17 throw new ProblemDetails(
18 status: ProblemDetails::BadRequest,
19 type: "/error/publickeyincorrecta.html",
20 title: "La publicKey debe ser texto.",
21 );
22
23 if (!isset($objeto->authToken) || !is_string($objeto->authToken))
24 throw new ProblemDetails(
25 status: ProblemDetails::BadRequest,
26 type: "/error/authtokenincorrecto.html",
27 title: "El authToken debe ser texto.",
28 );
29
30 if (!isset($objeto->contentEncoding) || !is_string($objeto->contentEncoding))
31 throw new ProblemDetails(
32 status: ProblemDetails::BadRequest,
33 type: "/error/contentencodingincorrecta.html",
34 title: "La contentEncoding debe ser texto.",
35 );
36
37 return new Suscripcion(
38 endpoint: $objeto->endpoint,
39 publicKey: $objeto->publicKey,
40 authToken: $objeto->authToken,
41 contentEncoding: $objeto->contentEncoding
42 );
43}
44

2. srv / modelo / Suscripcion.php

1<?php
2
3require_once __DIR__ . "/../../vendor/autoload.php";
4
5use Minishlink\WebPush\SubscriptionInterface;
6
7class Suscripcion implements SubscriptionInterface
8{
9
10 public string $endpoint;
11 public string $publicKey;
12 public string $authToken;
13 public string $contentEncoding;
14
15 public function __construct(
16 string $endpoint = "",
17 string $publicKey = "",
18 string $authToken = "",
19 string $contentEncoding = ""
20 ) {
21 $this->endpoint = $endpoint;
22 $this->publicKey = $publicKey;
23 $this->authToken = $authToken;
24 $this->contentEncoding = $contentEncoding;
25 }
26
27 public function getEndpoint(): string
28 {
29 return $this->endpoint;
30 }
31
32 public function getPublicKey(): ?string
33 {
34 return $this->publicKey;
35 }
36
37 public function getAuthToken(): ?string
38 {
39 return $this->authToken;
40 }
41
42 public function getContentEncoding(): ?string
43 {
44 return $this->contentEncoding;
45 }
46}
47

9. composer.json

1{
2 "require": {
3 "minishlink/web-push": "^8.0"
4 }
5}

10. composer.lock

1-- No se muestra el contenido de este archivo --

11. .replit

1run = "php -S 0.0.0.0:8000 -t ."
2
3entrypoint = "index.html"
4
5[nix]
6channel = "stable-23_05"
7
8[deployment]
9run = ["sh", "-c", "php -S 0.0.0.0:8000 -t ."]
10deploymentTarget = "cloudrun"
11
12[[ports]]
13localPort = 8000
14externalPort = 80
15

12. replit.nix

1{ pkgs }: {
2 deps = [
3 pkgs.php82
4 pkgs.php82Packages.composer
5];
6}

13. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / activaNotificacionesPush.js

1/**
2 * @param { string | URL } urlDeServiceWorkerQueRecibeNotificaciones
3 */
4export 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}

2. lib / js / calculaDtoParaSuscripcion.js

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 */
8export 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

3. lib / js / cancelaSuscripcionPush.js

1import { getSuscripcionPush } from "./getSuscripcionPush.js"
2
3export 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}

4. lib / js / enviaJson.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * @param { string } url
5 * @param { Object } body
6 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
7 * | "CONNECT" | "HEAD" } metodoHttp
8 */
9export async function enviaJson(url, body, metodoHttp = "POST") {
10 return await invocaServicio(fetch(url, {
11 method: metodoHttp,
12 headers: {
13 "Content-Type": "application/json",
14 "Accept": "application/json, application/problem+json"
15 },
16 body: JSON.stringify(body)
17 }))
18}
19
20// Permite que los eventos de html usen la función.
21window["enviaJson"] = enviaJson

5. lib / js / getSuscripcionPush.js

1export async function getSuscripcionPush() {
2 // Recupera el service worker registrado.
3 const registro = await navigator.serviceWorker.ready
4 return registro.pushManager.getSubscription()
5}

6. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

7. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

8. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

9. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

10. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

11. lib / js / suscribeAPush.js

1/**
2 * @param { Uint8Array } applicationServerKey
3 */
4export 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}

12. lib / js / urlBase64ToUint8Array.js

1/**
2 * @param { string } base64String
3 */
4export 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}

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeJson.php

1<?php
2
3function leeJson()
4{
5 return json_decode(file_get_contents("php://input"));
6}
7

4. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

14. Carpeta « error »

A. error / authtokenincorrecto.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 authToken debe ser texto</title>
10
11<body>
12
13 <h1>El authToken debe ser texto</h1>
14
15</body>
16
17</html>

B. error / contentencodingincorrecta.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>

C. error / endpointincorrecto.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>

D. error / errorinterno.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>

E. error / nojson.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 valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

F. error / publickeyincorrecta.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>

15. Carpeta « vendor »

A. vendor / -- No se muestra el contenido de esta carpeta --

1

16. jsconfig.json

  • 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": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

17. Resumen

  • En esta lección se muestró un ejemplo de notificaciones push.

12. Sincronización

Versión para imprimir.

A. Introducción

  • En esta lección se muestra un ejemplo de sincronización de bases de datos.

  • Puedes probar la app desde varios navegadores y dispositivos en https://replit.com/@GilbertoPachec2/sincronizacion?v=1. Hazle fork al proyecto y córrelo.

  • 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:

    Firebase
    PouchDb (navegador) + CouchDb (servidor)

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama de despliegue

Diagrama de despliegue

D. Hazlo funcionar

E. Archivos

F. index.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>Sincronizacion</title>
10
11 <script type="module" src="js/configura.js"></script>
12
13</head>
14
15<body>
16
17 <h1>Sincronizacion</h1>
18
19 <p><a href="agrega.html">Agregar</a></p>
20
21 <ul id="lista">
22 <li><progress max="100">Cargando…</progress></li>
23 </ul>
24
25 <script type="module">
26
27 import { muestraError } from "./lib/js/muestraError.js"
28 import { enviaJson } from "./lib/js/enviaJson.js"
29 import { Pasatiempo } from "./js/modelo/Pasatiempo.js"
30 import {
31 validaQueTengaLasPropiedadesDePasatiempo
32 } from "./js/modelo/validaQueTengaLasPropiedadesDePasatiempo.js"
33 import {
34 pasatiempoConsultaNoEliminados
35 } from "./js/bd/pasatiempoConsultaNoEliminados.js"
36 import { pasatiempoConsultaTodos } from "./js/bd/pasatiempoConsultaTodos.js"
37 import { pasatiemposReemplaza } from "./js/bd/pasatiemposReemplaza.js"
38
39 /**
40 * Cada 20 segundos (2000 milisegundos) después de la última
41 * sincronización, los datos se envían al servidor para volver a
42 * sincronizarse con los datos del servidor.
43 */
44 const MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR = 20000
45 let noHaSincronizado = true
46
47 // Crea y pone en funcionamiento el worker del archivo "render.js".
48 const worker = new Worker("render.js", { type: "module" })
49
50 // Se invoca cuando el worker envía un mensaje a la página.
51 worker.onmessage = async event => {
52 lista.innerHTML = event.data
53 if (noHaSincronizado) {
54 noHaSincronizado = false
55 await sincroniza()
56 } else {
57 esperaUnPocoYLanzaSincronizacion()
58 }
59 }
60
61 pasatiempoConsultaNoEliminados()
62 .then(renderEnWebWorker)
63 .catch(muestraError)
64
65 function renderEnWebWorker(datos) {
66 if (!Array.isArray(datos))
67 throw new Error("Los datos no están en un arreglo.")
68 for (const dato of datos) {
69 validaQueTengaLasPropiedadesDePasatiempo(dato)
70 }
71 // Manda los datos al worker para renderizar.
72 worker.postMessage(datos)
73 }
74
75 async function sincroniza() {
76 try {
77 if (navigator.onLine) {
78 const todos = await pasatiempoConsultaTodos()
79 const respuesta = await enviaJson("srv/srvSincro.php", todos)
80 const datosNuevos = respuesta.body
81 renderEnWebWorker(datosNuevos)
82 await pasatiemposReemplaza(datosNuevos)
83 } else {
84 esperaUnPocoYLanzaSincronizacion()
85 }
86 } catch (error) {
87 muestraError(error)
88 esperaUnPocoYLanzaSincronizacion()
89 }
90 }
91
92 function esperaUnPocoYLanzaSincronizacion() {
93 setTimeout(sincroniza, MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR)
94 }
95
96 </script>
97
98</body>
99
100</html>

G. render.js

1/* Ejemplo de render en el cliente. No se usa import
2 * porque Firefox no lo soporta en los web workers. */
3
4// Verifica si el código corre dentro de un web worker.
5if (self instanceof WorkerGlobalScope) {
6
7 // Se invoca al recibir un mensaje de la página.
8 onmessage = event => {
9
10 /**
11 * @type { {
12 * uuid: string,
13 * nombre: string,
14 * modificacion: number,
15 * eliminado: boolean,
16 * } [] }
17 */
18 const pasatiempos = event.data
19
20 let render = ""
21 for (const modelo of pasatiempos) {
22 const nombre = htmlentities(modelo.nombre)
23 const searchParams = new URLSearchParams([["uuid", modelo.uuid]])
24 const params = htmlentities(searchParams.toString())
25 render += /* html */
26 `<li>
27 <p><a href="modifica.html?${params}">${nombre}</a></p>
28 </li>`
29 }
30
31 // Envía el render a la página que invocó este web worker.
32 self.postMessage(render)
33
34 }
35
36}
37
38/**
39 * Codifica un texto para que cambie los caracteres
40 * especiales y no se pueda interpretar como
41 * etiiqueta HTML. Esta técnica evita la inyección
42 * de código.
43 * @param { string } texto
44 * @returns { string } un texto que no puede
45 * interpretarse como HTML. */
46export function htmlentities(texto) {
47 return texto.replace(/[<>"']/g, textoDetectado => {
48 switch (textoDetectado) {
49 case "<": return "<"
50 case ">": return ">"
51 case '"': return """
52 case "'": return "'"
53 default: return textoDetectado
54 }
55 })
56}

H. agrega.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/modelo/Pasatiempo.js"></script>
14 <script type="module" src="js/bd/pasatiempoAgrega.js"></script>
15
16</head>
17
18<body>
19
20 <form id="forma" onsubmit="
21 event.preventDefault()
22 // Lee el nombre, quitándole los espacios al inicio y al final.
23 const nombre = forma.nombre.value.trim()
24 const modelo = new Pasatiempo({ nombre })
25 pasatiempoAgrega(modelo)
26 .then(json => location.href = 'index.html')
27 .catch(muestraError)">
28
29 <h1>Agregar</h1>
30
31 <p><a href="index.html">Cancelar</a></p>
32
33 <p>
34 <label>
35 Nombre *
36 <input name="nombre">
37 </label>
38 </p>
39 <p>* Obligatorio</p>
40 <p><button type="submit">Agregar</button></p>
41
42 </form>
43
44</body>
45
46</html>

I. modifica.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="lib/js/confirmaEliminar.js"></script>
15 <script type="module" src="js/modelo/Pasatiempo.js"></script>
16 <script type="module" src="js/bd/pasatiempoBusca.js"></script>
17 <script type="module" src="js/bd/pasatiempoElimina.js"></script>
18 <script type="module" src="js/bd/pasatiempoModifica.js"></script>
19
20 <script>
21
22 // Obtiene los parámetros de la página.
23 const parametros = new URL(location.href).searchParams
24
25 const uuid = parametros.get("uuid")
26
27 </script>
28
29</head>
30
31<body onload="if (uuid !== null) {
32 pasatiempoBusca(uuid)
33 .then(async pasatiempo => {
34 if (pasatiempo === undefined) throw new Error('Pasatiempo no encontrado.')
35 await muestraObjeto(forma, { nombre: { value: pasatiempo.nombre } })
36 })
37 .catch(muestraError)
38 }">
39
40 <form id="forma" onsubmit="
41 event.preventDefault()
42 if (uuid !== null) {
43 // Lee el nombre, quitándole los espacios al inicio y al final.
44 const nombre = forma.nombre.value.trim()
45 const modelo = new Pasatiempo({ nombre, uuid })
46 pasatiempoModifica(modelo)
47 .then(json => location.href = 'index.html')
48 .catch(muestraError)
49 }">
50
51 <h1>Modificar</h1>
52
53 <p><a href="index.html">Cancelar</a></p>
54
55 <p>
56 <label>
57 Nombre *
58 <input name="nombre" value="Cargando…">
59 </label>
60 </p>
61
62 <p>* Obligatorio</p>
63
64 <p>
65
66 <button type="submit">Guardar</button>
67
68 <button type="button" onclick="if (uuid !== null && confirmaEliminar()) {
69 pasatiempoElimina(uuid)
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>

J. instruccionesListadoSw.txt

1Generar el listado de archivos del sw.js desde Visual Studio Code.
21. Abrir una terminal desde el menú
3 Terminal > New Terminal
4
52. Desde la terminal introducir la orden:
6 Get-ChildItem -path . -Recurse | Select Directory,Name | Out-File archivos.txt
7
83. Abrir el archivo generado, que se llama
9 archivos.txt
10 y sobre este, realizar los pasos que siguen:
11
124. Quita del archivo archivos.txt:
13 * el encabezado,
14 * todas las carpetas,
15 * el archivo .vscode/settings.json,
16 * el archivo archivos.txt,
17 * este archivo (instruccionesListadoSw.txt),
18 * el archivo jsconfig.json,
19 * el archivo sw.js,
20 * el archivo de la base de datos, que termina en ".bd" y
21 está en la carpeta srv,
22 * todos los archivos de php y
23 * las líneas en blanco del final
24
255. Cambia los \ por / desde Visual Studio Code con las siguientes
26 combinaciones de teclas:
27
28 Ctrl+H En el diálogo que aparece introduce lo siguiente:
29 Find:\
30 Replace:/
31
32 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
33
346. Coloca las comillas y coma del final de cada línea desde Visual
35 Studio Code con las siguientes combinaciones de teclas:
36
37 Ctrl+H En el diálogo que aparece, selecciona el botón
38 ".*"
39 e introduce lo siguiente:
40 Find:\s*$
41 Replace:",
42
43 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
44
457. Marca la carpeta inicial, presiona la combinación de teclas:
46
47 Shift+Ctrl+L
48
49 borra la selección, teclea " y luego ESC
50
518. Cambia las secuencias de espacios por / con las siguientes
52 combinaciones de teclas:
53
54 Ctrl+H En el diálogo que aparece, selecciona el botón
55 ".*"
56 e introduce lo siguiente:
57 Find:\s+
58 Replace:/
59
60 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
61
629. Cambia las "/ por " con las siguientes combinaciones de teclas:
63
64 Ctrl+H En el diálogo que aparece, quita la selección del botón
65 ".*"
66 e introduce lo siguiente:
67 Find:"/
68 Replace:"
69
70 Clic en el icono Reemplaza todo o Replace All y luego teclea ESC
71
7210. Copia el texto al archivo
73 sw.js
74 en el contenido del arreglo llamado ARCHIVOS, pero recuerda
75 mantener el último elemento, que dice:
76 "/"

K. archivos.txt

1"agrega.html",
2"index.html",
3"modifica.html",
4"render.js",
5"error/eliminadoincorrecto.html",
6"error/errorinterno.html",
7"error/modificacionincorrecta.html",
8"error/nojson.html",
9"error/nombreincorrecto.html",
10"error/uuidincorrecto.html",
11"js/configura.js",
12"js/bd/Bd.js",
13"js/bd/pasatiempoAgrega.js",
14"js/bd/pasatiempoBusca.js",
15"js/bd/pasatiempoConsultaNoEliminados.js",
16"js/bd/pasatiempoConsultaTodos.js",
17"js/bd/pasatiempoElimina.js",
18"js/bd/pasatiempoModifica.js",
19"js/bd/pasatiemposReemplaza.js",
20"js/modelo/leePasatiempo.js",
21"js/modelo/Pasatiempo.js",
22"js/modelo/validaQueTengaLasPropiedadesDePasatiempo.js",
23"lib/js/bdConsulta.js",
24"lib/js/bdEjecuta.js",
25"lib/js/confirmaEliminar.js",
26"lib/js/enviaJson.js",
27"lib/js/invocaServicio.js",
28"lib/js/JsonResponse.js",
29"lib/js/muestraError.js",
30"lib/js/muestraObjeto.js",
31"lib/js/ProblemDetails.js",
32"lib/js/registraServiceWorkerSiEsSoportado.js",

L. sw.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 */
18const VERSION = "1.00"
19
20/**
21 * Nombre de la carpeta de caché.
22 */
23const CACHE = "sincro"
24
25/**
26 * Archivos requeridos para que la aplicación funcione fuera de línea.
27 */
28const ARCHIVOS = [
29 "agrega.html",
30 "index.html",
31 "modifica.html",
32 "render.js",
33 "error/eliminadoincorrecto.html",
34 "error/errorinterno.html",
35 "error/modificacionincorrecta.html",
36 "error/nojson.html",
37 "error/nombreincorrecto.html",
38 "error/uuidincorrecto.html",
39 "js/configura.js",
40 "js/bd/Bd.js",
41 "js/bd/pasatiempoAgrega.js",
42 "js/bd/pasatiempoBusca.js",
43 "js/bd/pasatiempoConsultaNoEliminados.js",
44 "js/bd/pasatiempoConsultaTodos.js",
45 "js/bd/pasatiempoElimina.js",
46 "js/bd/pasatiempoModifica.js",
47 "js/bd/pasatiemposReemplaza.js",
48 "js/modelo/leePasatiempo.js",
49 "js/modelo/Pasatiempo.js",
50 "js/modelo/validaQueTengaLasPropiedadesDePasatiempo.js",
51 "lib/js/bdConsulta.js",
52 "lib/js/bdEjecuta.js",
53 "lib/js/confirmaEliminar.js",
54 "lib/js/enviaJson.js",
55 "lib/js/invocaServicio.js",
56 "lib/js/JsonResponse.js",
57 "lib/js/muestraError.js",
58 "lib/js/muestraObjeto.js",
59 "lib/js/ProblemDetails.js",
60 "lib/js/registraServiceWorkerSiEsSoportado.js",
61 "/"
62]
63
64// Verifica si el código corre dentro de un service worker.
65if (self instanceof ServiceWorkerGlobalScope) {
66 // Evento al empezar a instalar el servide worker,
67 self.addEventListener("install",
68 (/** @type {ExtendableEvent} */ evt) => {
69 console.log("El service worker se está instalando.")
70 evt.waitUntil(llenaElCache())
71 })
72
73 // Evento al solicitar información a la red.
74 self.addEventListener("fetch", (/** @type {FetchEvent} */ evt) => {
75 if (evt.request.method === "GET") {
76 evt.respondWith(buscaLaRespuestaEnElCache(evt))
77 }
78 })
79
80 // Evento cuando el service worker se vuelve activo.
81 self.addEventListener("activate",
82 () => console.log("El service worker está activo."))
83}
84
85async function llenaElCache() {
86 console.log("Intentando cargar caché:", CACHE, ".")
87 // Borra todos los cachés.
88 const keys = await caches.keys()
89 for (const key of keys) {
90 await caches.delete(key)
91 }
92 // Abre el caché de este service worker.
93 const cache = await caches.open(CACHE)
94 // Carga el listado de ARCHIVOS.
95 await cache.addAll(ARCHIVOS)
96 console.log("Cache cargado:", CACHE, ".")
97 console.log("Versión:", VERSION, ".")
98}
99
100/** @param {FetchEvent} evt */
101async function buscaLaRespuestaEnElCache(evt) {
102 // Abre el caché.
103 const cache = await caches.open(CACHE)
104 const request = evt.request
105 /* Busca la respuesta a la solicitud en el contenido del caché, sin
106 * tomar en cuenta la parte después del símbolo "?" en la URL. */
107 const response = await cache.match(request, { ignoreSearch: true })
108 if (response === undefined) {
109 /* Si no la encuentra, empieza a descargar de la red y devuelve
110 * la promesa. */
111 return fetch(request)
112 } else {
113 // Si la encuentra, devuelve la respuesta encontrada en el caché.
114 return response
115 }
116}

M. Carpeta « js »

A. js / configura.js

1import {
2 registraServiceWorkerSiEsSoportado
3} from "../lib/js/registraServiceWorkerSiEsSoportado.js"
4
5registraServiceWorkerSiEsSoportado("sw.js")

B. Carpeta « js / bd »

1. js / bd / Bd.js

1export const NOMBRE_DEL_ALMACEN_PASATIEMPO = "Pasatiempo"
2export const NOMBRE_DEL_INDICE_NOMBRE = "nombre"
3const BD_NOMBRE = "sincronizacion"
4const BD_VERSION = 1
5
6/** @type { Promise<IDBDatabase> } */
7export const Bd = new Promise((resolve, reject) => {
8
9 /* Se solicita abrir la base de datos, indicando nombre y
10 * número de versión. */
11 const solicitud = indexedDB.open(BD_NOMBRE, BD_VERSION)
12
13 // Si se presenta un error, rechaza la promesa.
14 solicitud.onerror = () => reject(solicitud.error)
15
16 // Si se abre con éxito, devuelve una conexión a la base de datos.
17 solicitud.onsuccess = () => resolve(solicitud.result)
18
19 // Si es necesario, se inicia una transacción para cambio de versión.
20 solicitud.onupgradeneeded = () => {
21
22 const bd = solicitud.result
23
24 // Como hay cambio de versión, borra el almacén si es que existe.
25 if (bd.objectStoreNames.contains(NOMBRE_DEL_ALMACEN_PASATIEMPO)) {
26 bd.deleteObjectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
27 }
28
29 // Crea el almacén "Pasatiempo" con el campo llave "uuid".
30 const almacenPasatiempo =
31 bd.createObjectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO, { keyPath: "uuid" })
32
33 // Crea un índice ordenado por el campo "nombre" que no acepta duplicados.
34 almacenPasatiempo.createIndex(NOMBRE_DEL_INDICE_NOMBRE, "nombre")
35 }
36
37})

2. js / bd / pasatiempoAgrega.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { Pasatiempo } from "../modelo/Pasatiempo.js"
3import { Bd, NOMBRE_DEL_ALMACEN_PASATIEMPO } from "./Bd.js"
4
5let secuencia = 0
6
7/**
8 * @param { Pasatiempo } modelo
9 */
10export async function pasatiempoAgrega(modelo) {
11 modelo.validaNuevo()
12 modelo.modificacion = Date.now()
13 modelo.eliminado = false
14
15 // Genera uuid único en internet.
16 modelo.uuid = Date.now().toString() + Math.random() + secuencia
17 secuencia++
18
19 return bdEjecuta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
20 transaccion => {
21 const almacenPasatiempo =
22 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
23 almacenPasatiempo.add(modelo)
24 })
25}
26
27// Permite que los eventos de html usen la función.
28window["pasatiempoAgrega"] = pasatiempoAgrega

3. js / bd / pasatiempoBusca.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import { leePasatiempo } from "../modelo/leePasatiempo.js"
3import { Pasatiempo } from "../modelo/Pasatiempo.js"
4import { Bd, NOMBRE_DEL_ALMACEN_PASATIEMPO } from "./Bd.js"
5
6/**
7 * @param { string | null } uuid
8 */
9export async function pasatiempoBusca(uuid) {
10
11 if (uuid === null)
12 throw new Error("Falta el uuid")
13
14 return bdConsulta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
15 /**
16 * @param { IDBTransaction } transaccion
17 * @param { (resultado: Pasatiempo|undefined) => any } resolve
18 */
19 (transaccion, resolve) => {
20
21 /* Pide el primer registro de almacén pasatiempo que tenga como
22 * llave primaria el valor del parámetro uuid. */
23 const consulta =
24 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO).get(uuid)
25
26 /* onsuccess se invoca solo una vez, devolviendo el registro
27 * solicitado. */
28 consulta.onsuccess = () => {
29
30 /* Se recupera el registro solicitado usando
31 * consulta.result
32 * Si el registro no se encuentra se recupera undefined. */
33 const objeto = consulta.result
34
35 if (objeto !== undefined) {
36 const modelo = leePasatiempo(objeto)
37 if (!modelo.eliminado) {
38 resolve(modelo)
39 }
40 }
41
42 resolve(undefined)
43
44 }
45
46 })
47
48}
49
50// Permite que los eventos de html usen la función.
51window["pasatiempoBusca"] = pasatiempoBusca

4. js / bd / pasatiempoConsultaNoEliminados.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import {
3 Bd,
4 NOMBRE_DEL_ALMACEN_PASATIEMPO, NOMBRE_DEL_INDICE_NOMBRE
5} from "./Bd.js"
6
7export async function pasatiempoConsultaNoEliminados() {
8
9 return bdConsulta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
10 /**
11 * @param { IDBTransaction } transaccion
12 * @param { (resultado: any[]) => any } resolve
13 */
14 (transaccion, resolve) => {
15
16 const resultado = []
17
18 const almacenPasatiempo =
19 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
20
21 // Usa el índice "nombre" para reculeprar los datos ordenadors.
22 const indiceNombre = almacenPasatiempo.index(NOMBRE_DEL_INDICE_NOMBRE)
23
24 /* Pide un cursor para recorrer cada uno de los registristros
25 * que devuelve la consulta. */
26 const consulta = indiceNombre.openCursor()
27
28 /* onsuccess se invoca por cada uno de los registros de la
29 * consulta y una vez cuando se acaban dichos registros. */
30 consulta.onsuccess = () => {
31
32 /* El cursor que corresponde al registro se recupera usando
33 * consulta.result */
34 const cursor = consulta.result
35
36 if (cursor === null) {
37
38 /* Si el cursor es igual a null, ya no hay más registros que
39 * procesar; por lo mismo, se devuelve el resultado con los
40 * pasatiempos recuperados, usando
41 * resolve(resultado). */
42 resolve(resultado)
43
44 } else {
45
46 /* Si el cursor es diferente a null, si hay más registros.
47 * El registro que sigue se obtiene con
48 * cursor.value */
49 const modelo = cursor.value
50 if (!modelo.eliminado) {
51 resultado.push(modelo)
52 }
53
54 /* Busca el siguiente registro de la consulta, que se obtiene
55 * la siguiente vez que se invoque la función
56 * onsuccess. */
57 cursor.continue()
58
59 }
60
61 }
62
63 })
64
65}
66
67// Permite que los eventos de html usen la función.
68window["pasatiempoConsultaNoEliminados"] = pasatiempoConsultaNoEliminados

5. js / bd / pasatiempoConsultaTodos.js

1import { bdConsulta } from "../../lib/js/bdConsulta.js"
2import { NOMBRE_DEL_ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
3
4/**
5 * Lista todos los objetos, incluyendo los que tienen
6 * borrado lógico.
7 */
8export async function pasatiempoConsultaTodos() {
9
10 return bdConsulta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
11 /**
12 * @param { IDBTransaction } transaccion
13 * @param { (result: any[]) => any } resolve
14 */
15 (transaccion, resolve) => {
16
17 const resultado = []
18
19 /* Pide un cursor para recorrer cada uno de los
20 * registristros que devuelve la consulta. */
21 const consulta =
22 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO).openCursor()
23
24 /* onsuccess se invoca por cada uno de los registros de la
25 * consulta y una vez cuando se acaban dichos registros. */
26 consulta.onsuccess = () => {
27
28 /* El cursor que corresponde al registro se recupera usando
29 * consulta.result */
30 const cursor = consulta.result
31
32 if (cursor === null) {
33
34 /* Si el cursor es igual a null, ya no hay más registros que
35 * procesar; por lo mismo, se devuelve el resultado con los
36 * pasatiempos recuperados, usando
37 * resolve(resultado). */
38 resolve(resultado)
39
40 } else {
41
42 /* Si el cursor es diferente a null, si hay más registros.
43 * El registro que sigue se obtiene con
44 * cursor.value*/
45 resultado.push(cursor.value)
46
47 /* Busca el siguiente registro de la consulta, que se obtiene
48 * la siguiente vez que se invoque la función
49 * onsuccess. */
50 cursor.continue()
51
52 }
53
54 }
55
56 })
57
58}

6. js / bd / pasatiempoElimina.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { NOMBRE_DEL_ALMACEN_PASATIEMPO, Bd } from "./Bd.js"
3import { pasatiempoBusca } from "./pasatiempoBusca.js"
4
5/**
6 * @param { string } uuid
7 */
8export async function pasatiempoElimina(uuid) {
9 const modelo = await pasatiempoBusca(uuid)
10 if (modelo !== undefined) {
11 modelo.modificacion = Date.now()
12 modelo.eliminado = true
13 return bdEjecuta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
14 transaccion => {
15 const almacenPasatiempo =
16 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
17 almacenPasatiempo.put(modelo)
18 })
19 }
20}
21
22// Permite que los eventos de html usen la función.
23window["pasatiempoElimina"] = pasatiempoElimina

7. js / bd / pasatiempoModifica.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { Pasatiempo } from "../modelo/Pasatiempo.js"
3import { Bd, NOMBRE_DEL_ALMACEN_PASATIEMPO } from "./Bd.js"
4import { pasatiempoBusca } from "./pasatiempoBusca.js"
5
6/**
7 * @param { Pasatiempo } modelo
8 */
9export async function pasatiempoModifica(modelo) {
10 modelo.valida()
11 const anterior = await pasatiempoBusca(modelo.uuid)
12 if (anterior === undefined) {
13 return undefined
14 } else {
15 modelo.modificacion = Date.now()
16 modelo.eliminado = false
17 return bdEjecuta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
18 transaccion => {
19 const almacenPasatiempo =
20 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
21 almacenPasatiempo.put(modelo)
22 })
23 }
24}
25
26// Permite que los eventos de html usen la función.
27window["pasatiempoModifica"] = pasatiempoModifica

8. js / bd / pasatiemposReemplaza.js

1import { bdEjecuta } from "../../lib/js/bdEjecuta.js"
2import { Bd, NOMBRE_DEL_ALMACEN_PASATIEMPO } from "./Bd.js"
3
4/**
5 * Borra el contenido del almacén pasatiempo y guarda el
6 * contenido del listado.
7 * @param {any[]} datosNuevos
8 */
9export async function pasatiemposReemplaza(datosNuevos) {
10 return bdEjecuta(Bd, [NOMBRE_DEL_ALMACEN_PASATIEMPO],
11 transaccion => {
12 const almacenPasatiempo =
13 transaccion.objectStore(NOMBRE_DEL_ALMACEN_PASATIEMPO)
14 almacenPasatiempo.clear()
15 for (const objeto of datosNuevos) {
16 almacenPasatiempo.add(objeto)
17 }
18 })
19}

C. Carpeta « js / modelo »

1. js / modelo / leePasatiempo.js

1import { Pasatiempo } from "./Pasatiempo.js"
2import {
3 validaQueTengaLasPropiedadesDePasatiempo
4} from "./validaQueTengaLasPropiedadesDePasatiempo.js"
5
6/** @param { any } objeto */
7export function leePasatiempo(objeto) {
8 const validado = validaQueTengaLasPropiedadesDePasatiempo(objeto)
9 return new Pasatiempo({
10 uuid: validado.uuid,
11 nombre: validado.nombre,
12 modificacion: validado.modificacion,
13 eliminado: validado.eliminado,
14 })
15}

2. js / modelo / Pasatiempo.js

1export class Pasatiempo {
2
3 /**
4 * @param { {
5 * uuid?: string,
6 * nombre?: string,
7 * modificacion?: number,
8 * eliminado?: boolean,
9 * } } modelo
10 */
11 constructor(modelo) {
12 this.nombre = modelo.nombre === undefined
13 ? ""
14 : modelo.nombre
15 this.uuid = modelo.uuid === undefined
16 ? ""
17 : modelo.uuid
18 this.modificacion = modelo.modificacion === undefined
19 ? NaN
20 : modelo.modificacion
21 this.eliminado = modelo.eliminado === undefined
22 ? true
23 : modelo.eliminado
24 }
25
26 validaNuevo() {
27 if (this.nombre === "")
28 throw new Error("Falta el nombre.")
29 }
30
31 valida() {
32 if (this.uuid === "")
33 throw new Error("Falta el uuid.")
34 if (this.nombre === "")
35 throw new Error("Falta el nombre.")
36 }
37
38}
39
40// Permite que los eventos de html usen la clase.
41window["Pasatiempo"] = Pasatiempo

3. js / modelo / validaQueTengaLasPropiedadesDePasatiempo.js

1/**
2 * @param { any } objeto
3 * @returns { {
4 * uuid: string,
5 * nombre: string,
6 * modificacion: number,
7 * eliminado: boolean,
8 * } }
9 */
10export function validaQueTengaLasPropiedadesDePasatiempo(objeto) {
11
12 if (typeof objeto.uuid !== "string")
13 throw new Error("El uuid debe ser texto.")
14
15 if (typeof objeto.nombre !== "string")
16 throw new Error("El nombre debe ser texto.")
17
18 if (typeof objeto.modificacion !== "number")
19 throw new Error("El campo modificacion debe ser número.")
20
21 if (typeof objeto.eliminado !== "boolean")
22 throw new Error("El campo eliminado debe ser booleano.")
23
24 return objeto
25
26}

N. Carpeta « srv »

A. srv / srvSincro.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/leeJson.php";
5require_once __DIR__ . "/modelo/Pasatiempo.php";
6require_once __DIR__ . "/modelo/leePasatiempo.php";
7require_once __DIR__ . "/bd/pasatiempoAgrega.php";
8require_once __DIR__ . "/bd/pasatiempoBusca.php";
9require_once __DIR__ . "/bd/pasatiempoConsultaNoEliminados.php";
10require_once __DIR__ . "/bd/pasatiempoModifica.php";
11
12ejecutaServicio(function () {
13 $lista = leeJson();
14 if (!is_array($lista)) {
15 $lista = [];
16 }
17 foreach ($lista as $objeto) {
18 $modeloEnElCliente = leePasatiempo($objeto);
19 $modeloEnElServidor = pasatiempoBusca($modeloEnElCliente->uuid);
20 if ($modeloEnElServidor === false) {
21 /* CONFLICTO. El objeto no ha estado en el servidor.
22 * AGREGARLO solamente si no está eliminado. */
23 if (!$modeloEnElCliente->eliminado) {
24 pasatiempoAgrega($modeloEnElCliente);
25 }
26 } elseif (
27 !$modeloEnElServidor->eliminado && $modeloEnElCliente->eliminado
28 ) {
29 /* CONFLICTO. El registro está en el servidor, donde no se ha
30 * eliminado, pero ha sido eliminado en el cliente.
31 * Gana el cliente, porque optamos por no revivir lo que se ha
32 * borrado. */
33 pasatiempoModifica($modeloEnElCliente);
34 } else if (
35 !$modeloEnElCliente->eliminado && !$modeloEnElServidor->eliminado
36 ) {
37 /* CONFLICTO. El registro está tanto en el servidor como en el
38 * cliente. Los datos pueden ser diferentes.
39 * PREVALECE LA FECHA MÁS GRANDE. Cuando gana el servidor no se
40 * hace nada.*/
41 if (
42 $modeloEnElCliente->modificacion > $modeloEnElServidor->modificacion
43 ) {
44 // La versión del cliente es más nueva y prevalece.
45 pasatiempoModifica($modeloEnElCliente);
46 }
47 }
48 }
49 $lista = pasatiempoConsultaNoEliminados();
50 return $lista;
51});
52

B. Carpeta « srv / bd »

1. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/bdCrea.php";
5
6class Bd
7{
8
9 private static ?PDO $conexion = null;
10
11 static function getConexion(): PDO
12 {
13 if (self::$conexion === null) {
14 self::$conexion = new PDO(
15 // cadena de conexión
16 "sqlite:sincronizacion.db",
17 // usuario
18 null,
19 // contraseña
20 null,
21 // Opciones: conexiones persistentes y lanza excepciones.
22 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
23 );
24
25 bdCrea(self::$conexion);
26 }
27
28 return self::$conexion;
29 }
30}
31

2. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS PASATIEMPO (
7 PAS_UUID TEXT NOT NULL,
8 PAS_NOMBRE TEXT NOT NULL,
9 PAS_MODIFICACION INTEGER NOT NULL,
10 PAS_ELIMINADO INTEGER NOT NULL,
11 CONSTRAINT PAS_PK
12 PRIMARY KEY(PAS_UUID)
13 )'
14 );
15}
16

3. srv / bd / pasatiempoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoAgrega(Pasatiempo $modelo) {
7 $modelo->valida();
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "INSERT INTO PASATIEMPO
11 (PAS_UUID, PAS_NOMBRE, PAS_MODIFICACION, PAS_ELIMINADO)
12 VALUES
13 (:uuid, :nombre, :modificacion, :eliminado)"
14 );
15 $stmt->execute([
16 ":uuid" => $modelo->uuid,
17 ":nombre" => $modelo->nombre,
18 ":modificacion" => $modelo->modificacion,
19 ":eliminado" => $modelo->eliminado ? 1 : 0
20 ]);
21}
22

4. srv / bd / pasatiempoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoBusca(string $uuid): false|Pasatiempo
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "SELECT
11 PAS_UUID AS uuid,
12 PAS_NOMBRE AS nombre,
13 PAS_MODIFICACION AS modificacion,
14 PAS_ELIMINADO AS eliminado
15 FROM PASATIEMPO
16 WHERE PAS_UUID = :uuid"
17 );
18 $stmt->execute([":uuid" => $uuid]);
19 $stmt->setFetchMode(
20 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
21 Pasatiempo::class
22 );
23 return $stmt->fetch();
24}
25

5. srv / bd / pasatiempoConsultaNoEliminados.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
6
7/** @return Pasatiempo[] */
8function pasatiempoConsultaNoEliminados()
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 PAS_UUID AS uuid,
14 PAS_NOMBRE AS nombre,
15 PAS_MODIFICACION AS modificacion,
16 PAS_ELIMINADO AS eliminado
17 FROM PASATIEMPO
18 WHERE PAS_ELIMINADO = 0
19 ORDER BY PAS_NOMBRE"
20 );
21 $resultado = $stmt->fetchAll(
22 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
23 Pasatiempo::class
24 );
25 return recibeFetchAll($resultado);
26}
27

6. srv / bd / pasatiempoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/Bd.php";
5
6function pasatiempoModifica(Pasatiempo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "UPDATE PASATIEMPO
12 SET
13 PAS_NOMBRE = :nombre,
14 PAS_MODIFICACION = :modificacion,
15 PAS_ELIMINADO = :eliminado
16 WHERE PAS_UUID = :uuid"
17 );
18 $stmt->execute([
19 ":uuid" => $modelo->uuid,
20 ":nombre" => $modelo->nombre,
21 ":modificacion" => $modelo->modificacion,
22 ":eliminado" => $modelo->eliminado ? 1 : 0
23 ]);
24}
25

C. Carpeta « srv / modelo »

1. srv / modelo / leePasatiempo.php

1<?php
2
3require_once __DIR__ . "/Pasatiempo.php";
4
5function leePasatiempo($objeto)
6{
7
8 if (!isset($objeto->nombre) || !is_string($objeto->nombre))
9 throw new ProblemDetails(
10 status: ProblemDetails::BadRequest,
11 type: "/error/nombreincorrecto.html",
12 title: "El nombre debe ser texto.",
13 );
14
15 if (!isset($objeto->uuid) || !is_string($objeto->uuid))
16 throw new ProblemDetails(
17 status: ProblemDetails::BadRequest,
18 type: "/error/uuidincorrecto.html",
19 title: "El uuid debe ser texto.",
20 );
21
22 if (!isset($objeto->eliminado) || !is_bool($objeto->eliminado))
23 throw new ProblemDetails(
24 status: ProblemDetails::BadRequest,
25 type: "/error/eliminadoincorrecto.html",
26 title: "El campo eliminado debe ser booleano.",
27 );
28
29 if (!isset($objeto->modificacion) || !is_int($objeto->modificacion))
30 throw new ProblemDetails(
31 status: ProblemDetails::BadRequest,
32 type: "/error/modificacionincorrecta.html",
33 title: "La modificacion debe ser número.",
34 );
35
36 return new Pasatiempo(
37 uuid: $objeto->uuid,
38 nombre: $objeto->nombre,
39 modificacion: $objeto->modificacion,
40 eliminado: $objeto->eliminado
41 );
42}
43

2. srv / modelo / Pasatiempo.php

1<?php
2
3class Pasatiempo
4{
5 public string $uuid;
6 public string $nombre;
7 public int $modificacion;
8 public bool $eliminado;
9
10 public function __construct(
11 string $nombre = "",
12 string $uuid = "",
13 int $modificacion = 0,
14 bool $eliminado = true
15 ) {
16 $this->nombre = $nombre;
17 $this->uuid = $uuid;
18 $this->modificacion = $modificacion;
19 $this->eliminado = $eliminado;
20 }
21
22 public function valida()
23 {
24 if ($this->uuid === "")
25 throw new Exception("Falta el uuid.");
26 if ($this->nombre === "")
27 throw new Exception("Falta el nombre.");
28 }
29}
30

O. Carpeta « lib »

A. Carpeta « lib / js »

1. lib / js / bdConsulta.js

1/**
2 * @template T
3 * @param { Promise<IDBDatabase> } bd
4 * @param { string[] } entidades
5 * @param { (
6 * transaccion: IDBTransaction,
7 * resolve: (resultado:T)=>void
8 * ) => void } consulta
9 * @returns {Promise<T>}
10 */
11export async function bdConsulta(bd, entidades, consulta) {
12
13 const base = await bd
14
15 return new Promise((resolve, reject) => {
16 // Inicia una transacción de solo lectura.
17 const transaccion = base.transaction(entidades, "readonly")
18 // Al terminar con error ejecuta la función reject.
19 transaccion.onerror = () => reject(transaccion.error)
20 // Estas son las operaciones para realizar la consulta.
21 consulta(transaccion, resolve)
22 })
23
24}

2. lib / js / bdEjecuta.js

1/**
2 * @param { Promise<IDBDatabase> } bd
3 * @param { string[] } entidades
4 * @param { (t:IDBTransaction) => void } operaciones
5 */
6export 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}

3. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

4. lib / js / enviaJson.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * @param { string } url
5 * @param { Object } body
6 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
7 * | "CONNECT" | "HEAD" } metodoHttp
8 */
9export async function enviaJson(url, body, metodoHttp = "POST") {
10 return await invocaServicio(fetch(url, {
11 method: metodoHttp,
12 headers: {
13 "Content-Type": "application/json",
14 "Accept": "application/json, application/problem+json"
15 },
16 body: JSON.stringify(body)
17 }))
18}
19
20// Permite que los eventos de html usen la función.
21window["enviaJson"] = enviaJson

5. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

6. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

7. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

8. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

9. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

10. lib / js / registraServiceWorkerSiEsSoportado.js

1import { muestraError } from "./muestraError.js"
2
3/**
4 * @param { string | URL } urlDeServiceWorker
5 */
6export async function registraServiceWorkerSiEsSoportado(urlDeServiceWorker) {
7 try {
8 if ("serviceWorker" in navigator) {
9 const registro = await navigator.serviceWorker.register(urlDeServiceWorker)
10 console.log(urlDeServiceWorker, "registrado.")
11 console.log(registro)
12 }
13 } catch (error) {
14 muestraError(error)
15 }
16}

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeJson.php

1<?php
2
3function leeJson()
4{
5 return json_decode(file_get_contents("php://input"));
6}
7

4. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

5. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

P. Carpeta « error »

A. error / eliminadoincorrecto.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 booleano</title>
10
11<body>
12
13 <h1>El campo eliminado debe ser booleano</h1>
14
15</body>
16
17</html>

B. error / errorinterno.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>

C. error / modificacionincorrecta.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>

D. error / nojson.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 valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

E. error / nombreincorrecto.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>

F. error / uuidincorrecto.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 uuid debe ser texto</title>
10
11<body>
12
13 <h1>El uuid debe ser texto</h1>
14
15</body>
16
17</html>

Q. jsconfig.json

  • 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": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

R. Resumen

  • En esta lección se muestra un ejemplo de sincronización de bases de datos.