A. Carpeta « lib / js »

Versión para imprimir.

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 / consumeJson.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Espera a que la promesa de un fetch termine. Si
5 * hay error, lanza una excepción. Si no hay error,
6 * interpreta la respuesta del servidor como JSON y
7 * la convierte en una literal de objeto.
8 *
9 * @param { string | Promise<Response> } servicio
10 */
11export async function consumeJson(servicio) {
12
13 if (typeof servicio === "string") {
14 servicio = fetch(servicio, {
15 headers: { "Accept": "application/json, application/problem+json" }
16 })
17 } else if (!(servicio instanceof Promise)) {
18 throw new Error("Servicio de tipo incorrecto.")
19 }
20
21 const respuesta = await servicio
22
23 const headers = respuesta.headers
24
25 if (respuesta.ok) {
26 // Aparentemente el servidor tuvo éxito.
27
28 if (respuesta.status === 204) {
29 // No contiene texto de respuesta.
30
31 return { headers, body: {} }
32
33 } else {
34
35 const texto = await respuesta.text()
36
37 try {
38
39 return { headers, body: JSON.parse(texto) }
40
41 } catch (error) {
42
43 // El contenido no es JSON. Probablemente sea texto de un error.
44 throw new ProblemDetails(respuesta.status, headers, texto,
45 "/error/errorinterno.html")
46
47 }
48
49 }
50
51 } else {
52 // Hay un error.
53
54 const texto = await respuesta.text()
55
56 if (texto === "") {
57
58 // No hay texto. Se usa el texto predeterminado.
59 throw new ProblemDetails(respuesta.status, headers, respuesta.statusText)
60
61 } else {
62 // Debiera se un ProblemDetails en JSON.
63
64 try {
65
66 const { title, type, detail } = JSON.parse(texto)
67
68 throw new ProblemDetails(respuesta.status, headers,
69 typeof title === "string" ? title : respuesta.statusText,
70 typeof type === "string" ? type : undefined,
71 typeof detail === "string" ? detail : undefined)
72
73 } catch (error) {
74
75 if (error instanceof ProblemDetails) {
76 // El error si era un ProblemDetails
77
78 throw error
79
80 } else {
81
82 throw new ProblemDetails(respuesta.status, headers, respuesta.statusText,
83 undefined, texto)
84
85 }
86
87 }
88
89 }
90
91 }
92
93}
94
95// Permite que los eventos de html usen la función.
96window["consumeJson"] = consumeJson

5. lib / js / enviaJson.js

1import { consumeJson } from "./consumeJson.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 consumeJson(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

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

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
10 if (error === null) {
11
12 console.log("Error")
13 alert("Error")
14
15 } else if (error instanceof ProblemDetails) {
16
17 let mensaje = error.title
18 if (error.detail) {
19 mensaje += `\n\n${error.detail}`
20 }
21 mensaje += `\n\nCódigo: ${error.status}`
22 if (error.type) {
23 mensaje += ` ${error.type}`
24 }
25
26 console.error(mensaje)
27 console.error(error)
28 console.error("Headers:")
29 error.headers.forEach((valor, llave) => console.error(llave, "=", valor))
30 alert(mensaje)
31
32 } else {
33
34 console.error(error)
35 alert(error.message)
36
37 }
38
39}
40
41// Permite que los eventos de html usen la función.
42window["muestraError"] = muestraError

8. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export function muestraObjeto(raizHtml, objeto) {
6
7 for (const [nombre, definiciones] of Object.entries(objeto)) {
8
9 if (Array.isArray(definiciones)) {
10
11 muestraArray(raizHtml, nombre, definiciones)
12
13 } else if (definiciones !== undefined && definiciones !== null) {
14
15 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
16
17 if (elementoHtml instanceof HTMLInputElement) {
18
19 muestraInput(raizHtml, elementoHtml, definiciones)
20
21 } else if (elementoHtml !== null) {
22
23 for (const [atributo, valor] of Object.entries(definiciones)) {
24 if (atributo in elementoHtml) {
25 elementoHtml[atributo] = valor
26 }
27 }
28
29 }
30
31 }
32
33 }
34
35}
36// Permite que los eventos de html usen la función.
37window["muestraObjeto"] = muestraObjeto
38
39/**
40 * @param { Document | HTMLElement } raizHtml
41 * @param { string } nombre
42 */
43export function buscaElementoHtml(raizHtml, nombre) {
44 return raizHtml.querySelector(
45 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
46}
47
48/**
49 * @param { Document | HTMLElement } raizHtml
50 * @param { string } propiedad
51 * @param {any[]} valores
52 */
53function muestraArray(raizHtml, propiedad, valores) {
54
55 const conjunto = new Set(valores)
56 const elementos =
57 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
58
59 if (elementos.length === 1) {
60 const elemento = elementos[0]
61
62 if (elemento instanceof HTMLSelectElement) {
63 const options = elemento.options
64 for (let i = 0, len = options.length; i < len; i++) {
65 const option = options[i]
66 option.selected = conjunto.has(option.value)
67 }
68 return
69 }
70
71 }
72
73 for (let i = 0, len = elementos.length; i < len; i++) {
74 const elemento = elementos[i]
75 if (elemento instanceof HTMLInputElement) {
76 elemento.checked = conjunto.has(elemento.value)
77 }
78 }
79
80}
81
82/**
83 * @param { Document | HTMLElement } raizHtml
84 * @param { HTMLInputElement } input
85 * @param { any } definiciones
86 */
87function muestraInput(raizHtml, input, definiciones) {
88
89 for (const [atributo, valor] of Object.entries(definiciones)) {
90
91 if (atributo == "data-file") {
92
93 const img = getImgParaElementoHtml(raizHtml, input)
94 if (img !== null) {
95 input.dataset.file = valor
96 input.value = ""
97 if (valor === "") {
98 img.src = ""
99 img.hidden = true
100 } else {
101 img.src = valor
102 img.hidden = false
103 }
104 }
105
106 } else if (atributo in input) {
107
108 input[atributo] = valor
109
110 }
111 }
112
113}
114
115/**
116 * @param { Document | HTMLElement } raizHtml
117 * @param { HTMLElement } elementoHtml
118 */
119export function getImgParaElementoHtml(raizHtml, elementoHtml) {
120 const imgId = elementoHtml.getAttribute("data-img")
121 if (imgId === null) {
122 return null
123 } else {
124 const input = buscaElementoHtml(raizHtml, imgId)
125 if (input instanceof HTMLImageElement) {
126 return input
127 } else {
128 return null
129 }
130 }
131}

9. lib / js / ProblemDetails.js

1/** Detalle de los errores devueltos por un servicio. */
2export class ProblemDetails extends Error {
3
4 /**
5 * @param {number} status
6 * @param {Headers} headers
7 * @param {string} title
8 * @param {string} [type]
9 * @param {string} [detail]
10 */
11 constructor(status, headers, title, type, detail) {
12 super(title)
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.headers = headers
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.detail = detail
21 /** @readonly */
22 this.title = title
23 }
24
25}

10. 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}

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