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

5. lib / js / enviaJson.js

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

6. lib / js / exportaAHtml.js

1/**
2 * Permite que los eventos de html usen la función.
3 * @param {function} functionInstance
4 */
5export function exportaAHtml(functionInstance) {
6 window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance
7}
8
9/**
10 * @param {function} valor
11 */
12export function nombreDeFuncionParaHtml(valor) {
13 const names = valor.name.split(/\s+/g)
14 return names[names.length - 1]
15}

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

8. lib / js / muestraError.js

1import { exportaAHtml } from "./exportaAHtml.js"
2import { ProblemDetails } from "./ProblemDetails.js"
3
4/**
5 * Muestra un error en la consola y en un cuadro de
6 * alerta el mensaje de una excepción.
7 * @param { ProblemDetails | Error | null } error descripción del error.
8 */
9export function muestraError(error) {
10
11 if (error === null) {
12
13 console.error("Error")
14 alert("Error")
15
16 } else if (error instanceof ProblemDetails) {
17
18 let mensaje = error.title
19 if (error.detail) {
20 mensaje += `\n\n${error.detail}`
21 }
22 mensaje += `\n\nCódigo: ${error.status}`
23 if (error.type) {
24 mensaje += ` ${error.type}`
25 }
26
27 console.error(mensaje)
28 console.error(error)
29 console.error("Headers:")
30 error.headers.forEach((valor, llave) => console.error(llave, "=", valor))
31 alert(mensaje)
32
33 } else {
34
35 console.error(error)
36 alert(error.message)
37
38 }
39
40}
41
42exportaAHtml(muestraError)

9. lib / js / muestraObjeto.js

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

10. lib / js / ProblemDetails.js

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

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}