| 1 | export const ALMACEN_PASATIEMPO = "PASATIEMPO" |
| 2 | export const PAS_ID = "PAS_ID" |
| 3 | export const INDICE_NOMBRE = "INDICE_NOMBRE" |
| 4 | export const PAS_NOMBRE = "PAS_NOMBRE" |
| 5 | const BD_NOMBRE = "sincro" |
| 6 | const BD_VERSION = 1 |
| 7 | |
| 8 | /** @type { Promise<IDBDatabase> } */ |
| 9 | export const Bd = new Promise((resolve, reject) => { |
| 10 | |
| 11 | /* Se solicita abrir la base de datos, indicando nombre y |
| 12 | * número de versión. */ |
| 13 | const solicitud = indexedDB.open(BD_NOMBRE, BD_VERSION) |
| 14 | |
| 15 | // Si se presenta un error, rechaza la promesa. |
| 16 | solicitud.onerror = () => reject(solicitud.error) |
| 17 | |
| 18 | // Si se abre con éxito, devuelve una conexión a la base de datos. |
| 19 | solicitud.onsuccess = () => resolve(solicitud.result) |
| 20 | |
| 21 | // Si es necesario, se inicia una transacción para cambio de versión. |
| 22 | solicitud.onupgradeneeded = () => { |
| 23 | |
| 24 | const bd = solicitud.result |
| 25 | |
| 26 | // Como hay cambio de versión, borra el almacén si es que existe. |
| 27 | if (bd.objectStoreNames.contains(ALMACEN_PASATIEMPO)) { |
| 28 | bd.deleteObjectStore(ALMACEN_PASATIEMPO) |
| 29 | } |
| 30 | |
| 31 | // Crea el almacén "PASATIEMPO" con el campo llave "PAS_ID". |
| 32 | const almacenPasatiempo = |
| 33 | bd.createObjectStore(ALMACEN_PASATIEMPO, { keyPath: PAS_ID }) |
| 34 | |
| 35 | // Crea un índice ordenado por el campo PAS_NOMBRE que no acepta duplicados. |
| 36 | almacenPasatiempo.createIndex(INDICE_NOMBRE, PAS_NOMBRE) |
| 37 | } |
| 38 | |
| 39 | }) |
| 1 | import { sincroniza } from "./sincroniza.js" |
| 2 | |
| 3 | /** |
| 4 | * Cada 20 segundos (2000 milisegundos) después de la última |
| 5 | * sincronización, los datos se envían al servidor para volver a |
| 6 | * sincronizarse con los datos del servidor. |
| 7 | */ |
| 8 | const MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR = 20000 |
| 9 | |
| 10 | export function esperaUnPocoYSincroniza() { |
| 11 | setTimeout(() => sincroniza(), MILISEGUNDOS_PARA_VOLVER_A_SINCRONIZAR) |
| 12 | } |
| 1 | /** |
| 2 | * @typedef {Object} PASATIEMPO |
| 3 | * @property {string} PAS_ID |
| 4 | * @property {string} PAS_NOMBRE |
| 5 | * @property {number} PAS_MODIFICACION |
| 6 | * @property {number} PAS_ELIMINADO |
| 7 | */ |
| 1 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 2 | import { bdEjecuta } from "./lib/bdEjecuta.js" |
| 3 | import { creaIdCliente } from "./lib/creaIdCliente.js" |
| 4 | import { recibeTextoObligatorio } from "./lib/recibeTextoObligatorio.js" |
| 5 | |
| 6 | |
| 7 | /** |
| 8 | * @param {SubmitEvent} event |
| 9 | */ |
| 10 | export async function pasatiempoAgrega(event) { |
| 11 | |
| 12 | event.preventDefault() |
| 13 | const target = event.target |
| 14 | |
| 15 | if (!(target instanceof HTMLFormElement)) |
| 16 | throw new Error("target no es de tipo form.") |
| 17 | |
| 18 | const formData = new FormData(target) |
| 19 | |
| 20 | const modelo = { |
| 21 | PAS_ID: creaIdCliente(Date.now().toString()), // Genera id único en internet. |
| 22 | PAS_NOMBRE: recibeTextoObligatorio(formData, "nombre"), |
| 23 | PAS_MODIFICACION: Date.now(), |
| 24 | PAS_ELIMINADO: 0, |
| 25 | } |
| 26 | |
| 27 | await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
| 28 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 29 | almacenPasatiempo.add(modelo) |
| 30 | }) |
| 31 | |
| 32 | location.href = "index.html" |
| 33 | |
| 34 | } |
| 1 | import { bdConsulta } from "./lib/bdConsulta.js" |
| 2 | import { validaPasatiempo } from "./validaPasatiempo.js" |
| 3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 4 | |
| 5 | /** |
| 6 | * @param {string} id |
| 7 | */ |
| 8 | export async function pasatiempoBusca(id) { |
| 9 | |
| 10 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
| 11 | /** |
| 12 | * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO|undefined) |
| 13 | * => any} resolve |
| 14 | */ |
| 15 | (transaccion, resolve) => { |
| 16 | |
| 17 | /* Pide el primer objeto de ALMACEN_PASATIEMPO que tenga como llave |
| 18 | * primaria el valor del parámetro id. */ |
| 19 | const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).get(id) |
| 20 | |
| 21 | // onsuccess se invoca solo una vez, devolviendo el objeto solicitado. |
| 22 | consulta.onsuccess = () => { |
| 23 | /* Se recupera el objeto solicitado usando |
| 24 | * consulta.result |
| 25 | * Si el objeto no se encuentra se recupera undefined. */ |
| 26 | const objeto = consulta.result |
| 27 | if (objeto !== undefined) { |
| 28 | const modelo = validaPasatiempo(objeto) |
| 29 | if (modelo.PAS_ELIMINADO === 0) { |
| 30 | resolve(modelo) |
| 31 | return |
| 32 | } |
| 33 | } |
| 34 | resolve(undefined) |
| 35 | |
| 36 | } |
| 37 | |
| 38 | }) |
| 39 | |
| 40 | } |
| 1 | import { ALMACEN_PASATIEMPO, Bd, INDICE_NOMBRE } from "./Bd.js" |
| 2 | import { bdConsulta } from "./lib/bdConsulta.js" |
| 3 | import { validaPasatiempo } from "./validaPasatiempo.js" |
| 4 | |
| 5 | export async function pasatiempoConsultaNoEliminados() { |
| 6 | |
| 7 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
| 8 | /** |
| 9 | * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO[])=>void |
| 10 | * } resolve |
| 11 | */ |
| 12 | (transaccion, resolve) => { |
| 13 | |
| 14 | const resultado = [] |
| 15 | |
| 16 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 17 | |
| 18 | // Usa el índice INDICE_NOMBRE para recuperar los datos ordenados. |
| 19 | const indiceNombre = almacenPasatiempo.index(INDICE_NOMBRE) |
| 20 | |
| 21 | // Pide un cursor para recorrer cada objeto que devuelve la consulta. |
| 22 | const consulta = indiceNombre.openCursor() |
| 23 | |
| 24 | /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez |
| 25 | * cuando se acaban dichos objetos. */ |
| 26 | consulta.onsuccess = () => { |
| 27 | /* El cursor correspondiente al objeto se recupera usando |
| 28 | * consulta.result */ |
| 29 | const cursor = consulta.result |
| 30 | if (cursor === null) { |
| 31 | /* Si el cursor vale null, ya no hay más objetos que procesar; por lo |
| 32 | * mismo, se devuelve el resultado con los pasatiempos recuperados, usando |
| 33 | * resolve(resultado). */ |
| 34 | resolve(resultado) |
| 35 | } else { |
| 36 | /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con |
| 37 | * cursor.value */ |
| 38 | const modelo = validaPasatiempo(cursor.value) |
| 39 | if (modelo.PAS_ELIMINADO === 0) { |
| 40 | resultado.push(modelo) |
| 41 | } |
| 42 | /* Busca el siguiente objeto de la consulta, que se recupera la siguiente |
| 43 | * vez que se invoque la función onsuccess. */ |
| 44 | cursor.continue() |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | }) |
| 49 | |
| 50 | } |
| 1 | import { bdConsulta } from "./lib/bdConsulta.js" |
| 2 | import { validaPasatiempo } from "./validaPasatiempo.js" |
| 3 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 4 | |
| 5 | /** |
| 6 | * Lista todos los objetos, incluyendo los que tienen borrado lógico. |
| 7 | */ |
| 8 | export async function pasatiempoConsultaTodos() { |
| 9 | |
| 10 | return bdConsulta(Bd, [ALMACEN_PASATIEMPO], |
| 11 | /** |
| 12 | * @param {(resultado: import("./PASATIEMPO.js").PASATIEMPO[])=>void |
| 13 | * } resolve |
| 14 | */ |
| 15 | (transaccion, resolve) => { |
| 16 | |
| 17 | const resultado = [] |
| 18 | |
| 19 | // Pide un cursor para recorrer cada objeto que devuelve la consulta. |
| 20 | const consulta = transaccion.objectStore(ALMACEN_PASATIEMPO).openCursor() |
| 21 | |
| 22 | /* onsuccess se invoca por cada uno de los objetos de la consulta y una vez |
| 23 | * cuando se acaban dichos objetos. */ |
| 24 | consulta.onsuccess = () => { |
| 25 | /* El cursor correspondiente al objeto se recupera usando |
| 26 | * consulta.result */ |
| 27 | const cursor = consulta.result |
| 28 | if (cursor === null) { |
| 29 | /* Si el cursor vale null, ya no hay más objetos que procesar; por lo |
| 30 | * mismo, se devuelve el resultado con los pasatiempos recuperados, usando |
| 31 | * resolve(resultado). */ |
| 32 | resolve(resultado) |
| 33 | } else { |
| 34 | /* Si el cursor no vale null y hay más objetos, el siguiente se obtiene con |
| 35 | * cursor.value*/ |
| 36 | resultado.push(validaPasatiempo(cursor.value)) |
| 37 | /* Busca el siguiente objeto de la consulta, que se recupera la siguiente |
| 38 | * vez que se invoque la función onsuccess. */ |
| 39 | cursor.continue() |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | }) |
| 44 | |
| 45 | } |
| 1 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 2 | import { bdEjecuta } from "./lib/bdEjecuta.js" |
| 3 | import { pasatiempoBusca } from "./pasatiempoBusca.js" |
| 4 | |
| 5 | /** |
| 6 | * @param { string } id |
| 7 | */ |
| 8 | export async function pasatiempoElimina(id) { |
| 9 | |
| 10 | if (confirm('Confirma la eliminación')) { |
| 11 | |
| 12 | const modelo = await pasatiempoBusca(id) |
| 13 | |
| 14 | if (modelo !== undefined) { |
| 15 | |
| 16 | modelo.PAS_MODIFICACION = Date.now() |
| 17 | modelo.PAS_ELIMINADO = 1 |
| 18 | await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
| 19 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 20 | almacenPasatiempo.put(modelo) |
| 21 | }) |
| 22 | |
| 23 | } |
| 24 | |
| 25 | location.href = "index.html" |
| 26 | |
| 27 | } |
| 28 | |
| 29 | } |
| 1 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 2 | import { bdEjecuta } from "./lib/bdEjecuta.js" |
| 3 | import { recibeTextoObligatorio } from "./lib/recibeTextoObligatorio.js" |
| 4 | import { pasatiempoBusca } from "./pasatiempoBusca.js" |
| 5 | |
| 6 | /** |
| 7 | * @param {SubmitEvent} event |
| 8 | * @param {string} id |
| 9 | */ |
| 10 | export async function pasatiempoModifica(event, id) { |
| 11 | |
| 12 | event.preventDefault() |
| 13 | const target = event.target |
| 14 | |
| 15 | if (!(target instanceof HTMLFormElement)) |
| 16 | throw new Error("target no es de tipo form.") |
| 17 | |
| 18 | const formData = new FormData(target) |
| 19 | |
| 20 | const nombre = recibeTextoObligatorio(formData, "nombre") |
| 21 | |
| 22 | const anterior = await pasatiempoBusca(id) |
| 23 | |
| 24 | if (anterior !== undefined) { |
| 25 | |
| 26 | anterior.PAS_NOMBRE = nombre |
| 27 | anterior.PAS_MODIFICACION = Date.now() |
| 28 | |
| 29 | await bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
| 30 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 31 | almacenPasatiempo.put(anterior) |
| 32 | }) |
| 33 | |
| 34 | location.href = "index.html" |
| 35 | |
| 36 | } |
| 37 | |
| 38 | } |
| 1 | import { bdEjecuta } from "./lib/bdEjecuta.js" |
| 2 | import { ALMACEN_PASATIEMPO, Bd } from "./Bd.js" |
| 3 | |
| 4 | /** |
| 5 | * Borra el contenido del almacén PASATIEMPO y guarda nuevospasatiempos. |
| 6 | * @param {import("./PASATIEMPO.js").PASATIEMPO[]} nuevosPasatiempos |
| 7 | */ |
| 8 | export async function pasatiemposReemplaza(nuevosPasatiempos) { |
| 9 | return bdEjecuta(Bd, [ALMACEN_PASATIEMPO], transaccion => { |
| 10 | const almacenPasatiempo = transaccion.objectStore(ALMACEN_PASATIEMPO) |
| 11 | almacenPasatiempo.clear() |
| 12 | for (const objeto of nuevosPasatiempos) { |
| 13 | almacenPasatiempo.add(objeto) |
| 14 | } |
| 15 | }) |
| 16 | } |
| 1 | import { htmlentities } from "./lib/htmlentities.js" |
| 2 | import { muestraObjeto } from "./lib/muestraObjeto.js" |
| 3 | |
| 4 | /** |
| 5 | * @param {import("./PASATIEMPO.js").PASATIEMPO[]} pasatiempos |
| 6 | */ |
| 7 | export function renderiza(pasatiempos) { |
| 8 | let render = "" |
| 9 | for (const modelo of pasatiempos) { |
| 10 | const nombre = htmlentities(modelo.PAS_NOMBRE) |
| 11 | const searchParams = new URLSearchParams([["id", modelo.PAS_ID]]) |
| 12 | const params = htmlentities(searchParams.toString()) |
| 13 | render += /* html */ |
| 14 | `<li> |
| 15 | <p><a href="modifica.html?${params}">${nombre}</a></p> |
| 16 | </li>` |
| 17 | } |
| 18 | muestraObjeto( |
| 19 | document, |
| 20 | { |
| 21 | lista: { innerHTML: render } |
| 22 | } |
| 23 | ) |
| 24 | } |
| 25 |
| 1 | import { pasatiempoConsultaTodos } from "./pasatiempoConsultaTodos.js" |
| 2 | import { pasatiemposReemplaza } from "./pasatiemposReemplaza.js" |
| 3 | import { esperaUnPocoYSincroniza } from "./esperaUnPocoYSincroniza.js" |
| 4 | import { consume } from "./lib/consume.js" |
| 5 | import { enviaJsonRecibeJson } from "./lib/enviaJsonRecibeJson.js" |
| 6 | import { muestraError } from "./lib/muestraError.js" |
| 7 | import { renderiza } from "./renderiza.js" |
| 8 | import { validaPasatiempos } from "./validaPasatiempos.js" |
| 9 | |
| 10 | export async function sincroniza() { |
| 11 | |
| 12 | try { |
| 13 | |
| 14 | if (navigator.onLine) { |
| 15 | const todos = await pasatiempoConsultaTodos() |
| 16 | const respuesta = |
| 17 | await consume(enviaJsonRecibeJson("php/sincroniza.php", todos)) |
| 18 | const pasatiempos = validaPasatiempos(await respuesta.json()) |
| 19 | await pasatiemposReemplaza(pasatiempos) |
| 20 | renderiza(pasatiempos) |
| 21 | } |
| 22 | |
| 23 | } catch (error) { |
| 24 | |
| 25 | muestraError(error) |
| 26 | |
| 27 | } |
| 28 | |
| 29 | esperaUnPocoYSincroniza() |
| 30 | |
| 31 | } |
| 1 | /** |
| 2 | * @param { any } objeto |
| 3 | * @returns {import("./PASATIEMPO.js").PASATIEMPO} |
| 4 | */ |
| 5 | export function validaPasatiempo(objeto) { |
| 6 | |
| 7 | if (typeof objeto.PAS_ELIMINADO !== "number" || isNaN(objeto.PAS_ELIMINADO)) |
| 8 | throw new Error("El campo eliminado debe ser número.") |
| 9 | |
| 10 | if (typeof objeto.PAS_ID !== "string" || objeto.PAS_ID === "") |
| 11 | throw new Error("El id debe ser texto que no esté en blanco.") |
| 12 | |
| 13 | if ( |
| 14 | typeof objeto.PAS_MODIFICACION !== "number" || isNaN(objeto.PAS_MODIFICACION) |
| 15 | ) |
| 16 | throw new Error("El campo modificacion debe ser número.") |
| 17 | |
| 18 | if (typeof objeto.PAS_NOMBRE !== "string" || objeto.PAS_ID === "") |
| 19 | throw new Error("El nombre debe ser texto que no esté en blanco.") |
| 20 | |
| 21 | return objeto |
| 22 | |
| 23 | } |
| 1 | import { validaPasatiempo } from "./validaPasatiempo.js" |
| 2 | |
| 3 | /** |
| 4 | * @param { any } objetos |
| 5 | * @returns {import("./PASATIEMPO.js").PASATIEMPO[]} |
| 6 | */ |
| 7 | export function validaPasatiempos(objetos) { |
| 8 | if (!Array.isArray(objetos)) |
| 9 | throw new Error("no se recibió un arreglo.") |
| 10 | /** |
| 11 | * @type {import("./PASATIEMPO.js").PASATIEMPO[]} |
| 12 | */ |
| 13 | const arreglo = [] |
| 14 | for (const objeto of objetos) { |
| 15 | arreglo.push(validaPasatiempo(objeto)) |
| 16 | } |
| 17 | return arreglo |
| 18 | } |
| 1 | /** |
| 2 | * @template T |
| 3 | * @param {Promise<IDBDatabase>} bd |
| 4 | * @param {string[]} almacenes |
| 5 | * @param {(transaccion: IDBTransaction, resolve: (resultado:T)=>void) => any |
| 6 | * } consulta |
| 7 | * @returns {Promise<T>} |
| 8 | */ |
| 9 | export async function bdConsulta(bd, almacenes, consulta) { |
| 10 | |
| 11 | const base = await bd |
| 12 | |
| 13 | return new Promise((resolve, reject) => { |
| 14 | // Inicia una transacción de solo lectura. |
| 15 | const transaccion = base.transaction(almacenes, "readonly") |
| 16 | // Al terminar con error ejecuta la función reject. |
| 17 | transaccion.onerror = () => reject(transaccion.error) |
| 18 | // Estas son las operaciones para realizar la consulta. |
| 19 | consulta(transaccion, resolve) |
| 20 | }) |
| 21 | |
| 22 | } |
| 1 | /** |
| 2 | * @param {Promise<IDBDatabase>} bd |
| 3 | * @param {string[]} entidades |
| 4 | * @param {(t:IDBTransaction) => void} operaciones |
| 5 | */ |
| 6 | export async function bdEjecuta(bd, entidades, operaciones) { |
| 7 | |
| 8 | // Espera que se abra la bd |
| 9 | const base = await bd |
| 10 | |
| 11 | return new Promise( |
| 12 | (resolve, reject) => { |
| 13 | // Inicia una transacción de lectura y escritura. |
| 14 | const transaccion = base.transaction(entidades, "readwrite") |
| 15 | // Al terminar con éxito, ejecuta la función resolve. |
| 16 | transaccion.oncomplete = resolve |
| 17 | // Al terminar con error, ejecuta la función reject. |
| 18 | transaccion.onerror = () => reject(transaccion.error) |
| 19 | // Estas son las operaciones de la transacción. |
| 20 | operaciones(transaccion) |
| 21 | }) |
| 22 | |
| 23 | } |
| 1 | import { 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 ( |
| 16 | contentType !== null && contentType.startsWith("application/problem+json") |
| 17 | ) |
| 18 | throw new ProblemDetailsError(await respuesta.json()) |
| 19 | else |
| 20 | throw new Error(respuesta.statusText) |
| 21 | } |
| 22 | } |
| 1 | /** |
| 2 | * Añade caracteres al azar a una raíz, para obtener un clientId único. |
| 3 | * @param {string} raiz |
| 4 | */ |
| 5 | export function creaIdCliente(raiz) { |
| 6 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
| 7 | for (var i = 0; i < 15; i++) { |
| 8 | raiz += chars.charAt(Math.floor(Math.random() * chars.length)) |
| 9 | } |
| 10 | return raiz |
| 11 | } |
| 1 | |
| 2 | /** |
| 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 | /** |
| 2 | * Codifica un texto para que cambie los caracteres |
| 3 | * especiales y no se pueda interpretar como |
| 4 | * etiiqueta HTML. Esta técnica evita la inyección |
| 5 | * de código. |
| 6 | * @param { string } texto |
| 7 | */ |
| 8 | export function htmlentities(texto) { |
| 9 | return texto.replace(/[<>"']/g, textoDetectado => { |
| 10 | switch (textoDetectado) { |
| 11 | case "<": return "<" |
| 12 | case ">": return ">" |
| 13 | case '"': return """ |
| 14 | case "'": return "'" |
| 15 | default: return textoDetectado |
| 16 | } |
| 17 | }) |
| 18 | } |
| 19 |
| 1 | import { 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 | * @param {FormData} formData |
| 3 | * @param {string} parametro |
| 4 | */ |
| 5 | export function recibeTexto(formData, parametro) { |
| 6 | const valor = formData.get(parametro) |
| 7 | if (valor !== null && typeof valor !== "string") |
| 8 | throw new Error(`El valor de ${parametro} debe ser texto.`) |
| 9 | return valor === null ? undefined : valor |
| 10 | } |
| 11 |
| 1 | import { recibeTexto } from "./recibeTexto.js" |
| 2 | |
| 3 | /** |
| 4 | * @param {FormData} formData |
| 5 | * @param {string} parametro |
| 6 | */ |
| 7 | export function recibeTextoObligatorio(formData, parametro) { |
| 8 | const texto = recibeTexto(formData, parametro) |
| 9 | if (texto === undefined) throw new Error(`Falta el valor de ${parametro}.`) |
| 10 | const trimTexto = texto.trim() |
| 11 | if (trimTexto === "") throw new Error(`Campo ${parametro} en blanco.`) |
| 12 | return trimTexto |
| 13 | } |
| 14 |
| 1 | const nombreDeServiceWorker = "sw.js" |
| 2 | |
| 3 | try { |
| 4 | navigator.serviceWorker.register(nombreDeServiceWorker) |
| 5 | .then(registro => { |
| 6 | console.log(nombreDeServiceWorker, "registrado.") |
| 7 | console.log(registro) |
| 8 | }) |
| 9 | .catch(error => console.log(error)) |
| 10 | } catch (error) { |
| 11 | console.log(error) |
| 12 | } |
| 1 | /** |
| 2 | * @template T |
| 3 | * @param {string} nombre |
| 4 | * @param {T | undefined} entidad |
| 5 | */ |
| 6 | export function validaEntidadObligatoria(nombre, entidad) { |
| 7 | |
| 8 | if (entidad === undefined) |
| 9 | throw new Error(`Registro de ${nombre} no encontrado.`) |
| 10 | |
| 11 | return entidad |
| 12 | } |
| 13 |