| 1 | /** |
| 2 | * @param { string | URL } urlDeServiceWorkerQueRecibeNotificaciones |
| 3 | */ |
| 4 | export async function activaNotificacionesPush( |
| 5 | |
| 6 | urlDeServiceWorkerQueRecibeNotificaciones) { |
| 7 | |
| 8 | // Valida que el navegador soporte notificaciones push. |
| 9 | if (!('PushManager' in window)) |
| 10 | throw new Error("Este navegador no soporta notificaciones push.") |
| 11 | |
| 12 | // Valida que el navegador soporte notificaciones, |
| 13 | if (!("Notification" in window)) |
| 14 | throw new Error("Este navegador no soporta notificaciones push.") |
| 15 | |
| 16 | // Valida que el navegador soporte service workers, |
| 17 | if (!("serviceWorker" in navigator)) |
| 18 | throw new Error("Este navegador no soporta service workers.") |
| 19 | |
| 20 | // Recupera el permiso para usar notificaciones |
| 21 | let permiso = Notification.permission |
| 22 | if (permiso === "default") { |
| 23 | // Permiso no asignado. Pide al usuario su autorización. |
| 24 | permiso = await Notification.requestPermission() |
| 25 | } |
| 26 | |
| 27 | // Valida que el usuario haya permitido usar notificaciones.. |
| 28 | if (permiso === "denied") |
| 29 | throw new Error("Notificaciones bloqueadas.") |
| 30 | |
| 31 | const registro = await navigator.serviceWorker.register( |
| 32 | urlDeServiceWorkerQueRecibeNotificaciones) |
| 33 | console.log(urlDeServiceWorkerQueRecibeNotificaciones, "registrado.") |
| 34 | console.log(registro) |
| 35 | |
| 36 | if (!("showNotification" in registro)) |
| 37 | throw new Error("Este navegador no soporta notificaciones.") |
| 38 | } |
| 1 | /** |
| 2 | * Devuelve una literal de objeto que se puede usar para enviar |
| 3 | * en formato JSON al servidor. |
| 4 | * DTO es un acrónimo para Data Transder Object, u |
| 5 | * objeto para transferencia de datos. |
| 6 | * @param { PushSubscription } suscripcion |
| 7 | */ |
| 8 | export function calculaDtoParaSuscripcion(suscripcion) { |
| 9 | const key = suscripcion.getKey("p256dh") |
| 10 | const token = suscripcion.getKey("auth") |
| 11 | const supported = PushManager.supportedContentEncodings |
| 12 | const encodings = Array.isArray(supported) && supported.length > 0 |
| 13 | ? supported |
| 14 | : ["aesgcm"] |
| 15 | const endpoint = suscripcion.endpoint |
| 16 | const publicKey = key === null |
| 17 | ? null |
| 18 | : btoa(String.fromCharCode.apply(null, new Uint8Array(key))) |
| 19 | const authToken = token === null |
| 20 | ? null |
| 21 | : btoa(String.fromCharCode.apply(null, new Uint8Array(token))) |
| 22 | const contentEncoding = encodings[0] |
| 23 | return { |
| 24 | endpoint, |
| 25 | publicKey, |
| 26 | authToken, |
| 27 | contentEncoding |
| 28 | } |
| 29 | } |
| 30 |
| 1 | import { getSuscripcionPush } from "./getSuscripcionPush.js" |
| 2 | |
| 3 | export async function cancelaSuscripcionPush() { |
| 4 | const suscripcion = await getSuscripcionPush() |
| 5 | const resultado = suscripcion === null |
| 6 | ? false |
| 7 | : await suscripcion.unsubscribe() |
| 8 | return resultado === true ? suscripcion : null |
| 9 | } |
| 1 | import { ProblemDetailsError } from "./ProblemDetailsError.js" |
| 2 | |
| 3 | /** |
| 4 | * Espera a que la promesa de un fetch termine. Si |
| 5 | * hay error, lanza una excepción. |
| 6 | * |
| 7 | * @param {Promise<Response> } servicio |
| 8 | */ |
| 9 | export async function consume(servicio) { |
| 10 | const respuesta = await servicio |
| 11 | if (respuesta.ok) { |
| 12 | return respuesta |
| 13 | } else { |
| 14 | const contentType = respuesta.headers.get("Content-Type") |
| 15 | if (contentType === "application/problem+json; charset=utf-8") |
| 16 | throw new ProblemDetailsError(await respuesta.json()) |
| 17 | else |
| 18 | throw new Error(respuesta.statusText) |
| 19 | } |
| 20 | } |
| 1 | import { consume } from "./consume.js" |
| 2 | import { muestraObjeto } from "./muestraObjeto.js" |
| 3 | import { recibeJson } from "./recibeJson.js" |
| 4 | |
| 5 | /** |
| 6 | * @param {string} url |
| 7 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
| 8 | * | "CONNECT" | "HEAD" } metodoHttp |
| 9 | */ |
| 10 | export async function descargaVista(url, metodoHttp = "GET") { |
| 11 | const respuesta = await consume(recibeJson(url, metodoHttp)) |
| 12 | const json = await respuesta.json() |
| 13 | muestraObjeto(document, json) |
| 14 | return json |
| 15 | } |
| 1 | |
| 2 | /** |
| 3 | * @param { string } url |
| 4 | * @param { Object } body |
| 5 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
| 6 | * | "CONNECT" | "HEAD" } metodoHttp |
| 7 | */ |
| 8 | export async function enviaJsonRecibeJson(url, body, metodoHttp = "POST") { |
| 9 | return fetch( |
| 10 | url, |
| 11 | { |
| 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 | } |
| 1 | export async function getSuscripcionPush() { |
| 2 | // Recupera el service worker registrado. |
| 3 | const registro = await navigator.serviceWorker.ready |
| 4 | return registro.pushManager.getSubscription() |
| 5 | } |
| 1 | import { muestraError } from "./muestraError.js" |
| 2 | |
| 3 | /** |
| 4 | * Intercepta Response.prototype.json para capturar errores de parseo |
| 5 | * y asegurar que se reporten correctamente en navegadores Chromium. |
| 6 | */ |
| 7 | { |
| 8 | const originalJson = Response.prototype.json |
| 9 | |
| 10 | Response.prototype.json = function () { |
| 11 | // Llamamos al método original usando el contexto (this) de la respuesta |
| 12 | return originalJson.apply(this, arguments) |
| 13 | .catch((/** @type {any} */ error) => { |
| 14 | // Corrige un error de Chrome que evita el manejo correcto de errores. |
| 15 | throw new Error(error) |
| 16 | }) |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | window.onerror = function ( |
| 21 | /** @type {string} */ _message, |
| 22 | /** @type {string} */ _url, |
| 23 | /** @type {number} */ _line, |
| 24 | /** @type {number} */ _column, |
| 25 | /** @type {Error} */ errorObject |
| 26 | ) { |
| 27 | muestraError(errorObject) |
| 28 | return true |
| 29 | } |
| 30 | |
| 31 | window.addEventListener('unhandledrejection', event => { |
| 32 | muestraError(event.reason) |
| 33 | event.preventDefault() |
| 34 | }) |
| 35 |
| 1 | import { ProblemDetailsError } from "./ProblemDetailsError.js" |
| 2 | |
| 3 | /** |
| 4 | * Muestra los datos de una Error en la consola y en un cuadro de alerta. |
| 5 | * @param { ProblemDetailsError | Error | null } error descripción del error. |
| 6 | */ |
| 7 | export function muestraError(error) { |
| 8 | |
| 9 | if (error === null) { |
| 10 | |
| 11 | console.error("Error") |
| 12 | alert("Error") |
| 13 | |
| 14 | } else if (error instanceof ProblemDetailsError) { |
| 15 | |
| 16 | const problemDetails = error.problemDetails |
| 17 | |
| 18 | let mensaje = |
| 19 | typeof problemDetails["title"] === "string" ? problemDetails["title"] : "" |
| 20 | if (typeof problemDetails["detail"] === "string") { |
| 21 | if (mensaje !== "") { |
| 22 | mensaje += "\n\n" |
| 23 | } |
| 24 | mensaje += problemDetails["detail"] |
| 25 | } |
| 26 | if (mensaje === "") { |
| 27 | mensaje = "Error" |
| 28 | } |
| 29 | console.error(error, problemDetails) |
| 30 | alert(mensaje) |
| 31 | |
| 32 | } else { |
| 33 | |
| 34 | console.error(error) |
| 35 | alert(error.message) |
| 36 | |
| 37 | } |
| 38 | |
| 39 | } |
| 1 | /** |
| 2 | * @param {Document | HTMLElement | ShadowRoot} raizHtml |
| 3 | * @param { any } objeto |
| 4 | */ |
| 5 | export 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 | muestraElemento(raizHtml, nombre, definiciones) |
| 11 | } |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * @param { string } nombre |
| 17 | */ |
| 18 | export function selectorDeNombre(nombre) { |
| 19 | return `[id="${nombre}"],[name="${nombre}"],[data-name="${nombre}"]` |
| 20 | } |
| 21 | |
| 22 | /** |
| 23 | * @param { Document | HTMLElement | ShadowRoot } raizHtml |
| 24 | * @param { string } propiedad |
| 25 | * @param {any[]} valores |
| 26 | */ |
| 27 | function muestraArray(raizHtml, propiedad, valores) { |
| 28 | const conjunto = new Set(valores) |
| 29 | const elementos = raizHtml.querySelectorAll(selectorDeNombre(propiedad)) |
| 30 | if (elementos.length === 1 && elementos[0] instanceof HTMLSelectElement) { |
| 31 | muestraOptions(elementos[0], conjunto) |
| 32 | } else { |
| 33 | muestraInputs(elementos, conjunto) |
| 34 | } |
| 35 | |
| 36 | } |
| 37 | |
| 38 | /** |
| 39 | * @param {HTMLSelectElement} select |
| 40 | * @param {Set<any>} conjunto |
| 41 | */ |
| 42 | function muestraOptions(select, conjunto) { |
| 43 | for (let i = 0, options = select.options, len = options.length; i < len; i++) { |
| 44 | const option = options[i] |
| 45 | option.selected = conjunto.has(option.value) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * @param {NodeListOf<Element>} elementos |
| 51 | * @param {Set<any>} conjunto |
| 52 | */ |
| 53 | function muestraInputs(elementos, conjunto) { |
| 54 | for (let i = 0, len = elementos.length; i < len; i++) { |
| 55 | const elemento = elementos[i] |
| 56 | if (elemento instanceof HTMLInputElement) { |
| 57 | elemento.checked = conjunto.has(elemento.value) |
| 58 | } |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | const data_ = "data-" |
| 63 | const data_Length = data_.length |
| 64 | |
| 65 | /** |
| 66 | * @param {Document | HTMLElement | ShadowRoot} raizHtml |
| 67 | * @param {string} nombre |
| 68 | * @param {{ [s: string]: any; } } definiciones |
| 69 | */ |
| 70 | function muestraElemento(raizHtml, nombre, definiciones) { |
| 71 | const elemento = raizHtml.querySelector(selectorDeNombre(nombre)) |
| 72 | if (elemento !== null) { |
| 73 | for (const [propiedad, valor] of Object.entries(definiciones)) { |
| 74 | if (propiedad in elemento) { |
| 75 | elemento[propiedad] = valor |
| 76 | } else if ( |
| 77 | propiedad.length > data_Length |
| 78 | && propiedad.startsWith(data_) |
| 79 | && elemento instanceof HTMLElement |
| 80 | ) { |
| 81 | elemento.dataset[propiedad.substring(data_Length)] = valor |
| 82 | } |
| 83 | } |
| 84 | } |
| 85 | } |
| 1 | export class ProblemDetailsError extends Error { |
| 2 | |
| 3 | /** |
| 4 | * Detalle de los errores devueltos por un servicio. |
| 5 | * Crea una instancia de ProblemDetailsError. |
| 6 | * @param {object} problemDetails Objeto con la descripcipon del error. |
| 7 | */ |
| 8 | constructor(problemDetails) { |
| 9 | |
| 10 | super(typeof problemDetails["detail"] === "string" |
| 11 | ? problemDetails["detail"] |
| 12 | : (typeof problemDetails["title"] === "string" |
| 13 | ? problemDetails["title"] |
| 14 | : "Error")) |
| 15 | |
| 16 | this.problemDetails = problemDetails |
| 17 | |
| 18 | } |
| 19 | |
| 20 | } |
| 1 | |
| 2 | /** |
| 3 | * @param {string} url |
| 4 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
| 5 | * | "CONNECT" | "HEAD" } metodoHttp |
| 6 | */ |
| 7 | export async function recibeJson(url, metodoHttp = "GET") { |
| 8 | return fetch( |
| 9 | url, |
| 10 | { |
| 11 | method: metodoHttp, |
| 12 | headers: { "Accept": "application/json, application/problem+json" } |
| 13 | } |
| 14 | ) |
| 15 | } |
| 1 | /** |
| 2 | * @param { ArrayBuffer } applicationServerKey |
| 3 | */ |
| 4 | export async function suscribeAPush(applicationServerKey) { |
| 5 | // Recupera el service worker registrado. |
| 6 | const registro = await navigator.serviceWorker.ready |
| 7 | return registro.pushManager.subscribe({ |
| 8 | userVisibleOnly: true, |
| 9 | applicationServerKey |
| 10 | }) |
| 11 | } |
| 1 | /** |
| 2 | * @param { string } base64String |
| 3 | */ |
| 4 | export function urlBase64ToUint8Array(base64String) { |
| 5 | const padding = "=".repeat((4 - (base64String.length % 4)) % 4) |
| 6 | const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/') |
| 7 | const rawData = atob(base64) |
| 8 | const outputArray = new Uint8Array(rawData.length) |
| 9 | for (let i = 0; i < rawData.length; ++i) { |
| 10 | outputArray[i] = rawData.charCodeAt(i) |
| 11 | } |
| 12 | return outputArray |
| 13 | } |