| 1 | import { resaltaSiEstasEn } from "./lib/resaltaSiEstasEn.js" |
| 2 | |
| 3 | export class NavTabFixed extends HTMLElement { |
| 4 | |
| 5 | constructor() { |
| 6 | super() |
| 7 | this.creado = false |
| 8 | } |
| 9 | |
| 10 | connectedCallback() { |
| 11 | this.classList.add("md-tab", "fixed") |
| 12 | |
| 13 | if (!this.creado) { |
| 14 | |
| 15 | this.innerHTML = /* HTML */` |
| 16 | <a ${resaltaSiEstasEn(["/index.html", "", "/"])} href="index.html"> |
| 17 | <span class="material-symbols-outlined">home</span> |
| 18 | Inicio |
| 19 | </a> |
| 20 | |
| 21 | <a ${resaltaSiEstasEn(["/ayuda.html"])} href="ayuda.html"> |
| 22 | <span class="material-symbols-outlined">help</span> |
| 23 | Ayuda |
| 24 | </a>` |
| 25 | |
| 26 | this.creado = true |
| 27 | |
| 28 | } |
| 29 | |
| 30 | } |
| 31 | |
| 32 | } |
| 33 | |
| 34 | customElements.define("nav-tab-fixed", NavTabFixed) |
| 1 | export const ES_APPLE = /.*(iPad|iPhone|iPod|Mac).*/.test(navigator.userAgent) |
| 1 | /** |
| 2 | * @param {HTMLElement} elementoHtml |
| 3 | * @param {string} nombre |
| 4 | * @returns {string} |
| 5 | */ |
| 6 | export function getAttribute(elementoHtml, nombre) { |
| 7 | const valor = elementoHtml.getAttribute(nombre) |
| 8 | return valor === null ? "" : valor |
| 9 | } |
| 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 | * 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 |
| 12 | * estado de elementoHtml es válido. |
| 13 | */ |
| 14 | export function muestraTextoDeAyuda(elementoHtml, elementoDeAyuda, |
| 15 | mensajeDeAyuda) { |
| 16 | if (elementoHtml.validity.valid) { |
| 17 | elementoDeAyuda.textContent = mensajeDeAyuda |
| 18 | } else { |
| 19 | elementoDeAyuda.textContent = elementoHtml.validationMessage |
| 20 | } |
| 21 | } |
| 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 | * @template { HTMLElement } T |
| 3 | * @param { Document | Element | ShadowRoot } raiz |
| 4 | * @param { string } query |
| 5 | * @returns { T } |
| 6 | */ |
| 7 | export 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 | } |
| 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 | * @param {string[]} paginas |
| 3 | */ |
| 4 | export function resaltaSiEstasEn(paginas) { |
| 5 | |
| 6 | const pathname = location.pathname |
| 7 | |
| 8 | for (const pagina of paginas) { |
| 9 | |
| 10 | if (pathname === pagina) { |
| 11 | queueMicrotask(() => { |
| 12 | const tab = document.querySelector(".active") |
| 13 | if (tab !== null && tab.closest(".scrollable") !== null) { |
| 14 | tab.scrollIntoView({ inline: "center", block: "end" }) |
| 15 | } |
| 16 | }) |
| 17 | return `class="active"` |
| 18 | } |
| 19 | |
| 20 | } |
| 21 | |
| 22 | return "" |
| 23 | |
| 24 | } |
| 1 | import { ES_APPLE } from "../ES_APPLE.js" |
| 2 | import { getAttribute } from "../getAttribute.js" |
| 3 | import { querySelector } from "../querySelector.js" |
| 4 | |
| 5 | class MdAppBar 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 0.25rem; |
| 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, 4rem); |
| 22 | width: env(titlebar-area-width, 100%); |
| 23 | } |
| 24 | |
| 25 | :host(.apple) { |
| 26 | height: env(titlebar-area-height, 3rem); |
| 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(.centered) #navigation, |
| 49 | :host(.centered) #acciones { |
| 50 | flex: 0 0 6rem; |
| 51 | overflow: hidden |
| 52 | } |
| 53 | |
| 54 | #headline::slotted(*) { |
| 55 | -webkit-app-region: drag; |
| 56 | flex: 1 1 auto; |
| 57 | white-space: nowrap; |
| 58 | text-overflow: ellipsis; |
| 59 | overflow: hidden; |
| 60 | font-family: var(--md-sys-typescale-title-large-font); |
| 61 | font-weight: var(--md-sys-typescale-title-large-weight); |
| 62 | font-size: var(--md-sys-typescale-title-large-size); |
| 63 | font-style: var(--md-sys-typescale-title-large-font-style); |
| 64 | letter-spacing: var(--md-sys-typescale-title-large-tracking); |
| 65 | line-height: var(--md-sys-typescale-title-large-line-height); |
| 66 | text-transform: var(--md-sys-typescale-title-large-text-transform); |
| 67 | text-decoration: var(--md-sys-typescale-title-large-text-decoration); |
| 68 | color: var(--md-sys-color-on-surface); |
| 69 | } |
| 70 | |
| 71 | :host(.centered) #headline::slotted(*) { |
| 72 | flex: 1 1 auto; |
| 73 | text-align: center |
| 74 | } |
| 75 | |
| 76 | </style> |
| 77 | |
| 78 | <span id="navigation"> |
| 79 | <slot name="navigation"></slot> |
| 80 | </span> |
| 81 | <slot id="headline"></slot> |
| 82 | <span id="acciones"> |
| 83 | <slot name="action"></slot> |
| 84 | </span>` |
| 85 | } |
| 86 | |
| 87 | constructor() { |
| 88 | super() |
| 89 | if (ES_APPLE) { |
| 90 | document.body.classList.add("apple") |
| 91 | document.body.classList.remove("material") |
| 92 | } else { |
| 93 | document.body.classList.add("material") |
| 94 | document.body.classList.remove("apple") |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * @private |
| 99 | * @readonly |
| 100 | */ |
| 101 | const shadow = this.attachShadow({ mode: "open" }) |
| 102 | shadow.innerHTML = this.getContent() |
| 103 | this._configuraAction = this._configuraAction.bind(this) |
| 104 | /** |
| 105 | * @private |
| 106 | * @type {number} |
| 107 | */ |
| 108 | this._posY = 0 |
| 109 | /** |
| 110 | * @private |
| 111 | * @type {boolean} |
| 112 | */ |
| 113 | this._scrolling = false |
| 114 | /** |
| 115 | * @private |
| 116 | * @type { HTMLSlotElement } |
| 117 | */ |
| 118 | this._navigation = querySelector(shadow, '[name="navigation"]') |
| 119 | /** |
| 120 | * @private |
| 121 | * @type { HTMLSlotElement } |
| 122 | */ |
| 123 | this._action = querySelector(shadow, '[name="action"]') |
| 124 | /** |
| 125 | * @private |
| 126 | * @type { HTMLHeadingElement | null } |
| 127 | */ |
| 128 | this._headline = null |
| 129 | /** |
| 130 | * @private |
| 131 | * @type { HTMLElement | null } |
| 132 | */ |
| 133 | this._adicional = null |
| 134 | this._action.addEventListener("slotchange", this._configuraAction) |
| 135 | addEventListener("scroll", () => this._onScroll()) |
| 136 | addEventListener("load", () => this.configurOtros()) |
| 137 | } |
| 138 | |
| 139 | connectedCallback() { |
| 140 | this.role = "toolbar" |
| 141 | this._configuraAction() |
| 142 | } |
| 143 | |
| 144 | configurOtros() { |
| 145 | const idHeadline = getAttribute(this, "headline") |
| 146 | if (idHeadline !== "") { |
| 147 | const headline = document.getElementById(idHeadline) |
| 148 | if (headline instanceof HTMLHeadingElement) { |
| 149 | this._headline = headline |
| 150 | if (this.classList.contains("apple") || this.classList.contains("medium")) { |
| 151 | headline.classList.add("md-headline", "headline-small") |
| 152 | } else { |
| 153 | headline.classList.add("md-headline", "headline-medium") |
| 154 | } |
| 155 | } |
| 156 | } |
| 157 | const idAdicional = getAttribute(this, "adicional") |
| 158 | if (idAdicional !== "") { |
| 159 | this._adicional = document.getElementById(idAdicional) |
| 160 | if (this._adicional !== null) { |
| 161 | if (this.classList.contains("apple")) { |
| 162 | this._adicional.style.top = "env(titlebar-area-height, 3rem)" |
| 163 | } else { |
| 164 | this._adicional.style.top = "env(titlebar-area-height, 4rem)" |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | _configuraAction() { |
| 171 | const assignedElements = this._action.assignedElements() |
| 172 | if (this.isConnected) { |
| 173 | if (ES_APPLE) { |
| 174 | this.classList.add("apple") |
| 175 | this.classList.remove("material") |
| 176 | } else { |
| 177 | this.classList.add("material") |
| 178 | this.classList.remove("apple") |
| 179 | } |
| 180 | if (this.classList.contains("centered")) { |
| 181 | this.classList.remove("centrado") |
| 182 | this.classList.remove("justificado") |
| 183 | } else { |
| 184 | if (ES_APPLE && assignedElements.length <= 1) { |
| 185 | this.classList.add("centrado") |
| 186 | this.classList.remove("justificado") |
| 187 | } else { |
| 188 | this.classList.add("justificado") |
| 189 | this.classList.remove("centrado") |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | /** @private */ |
| 196 | _onScroll() { |
| 197 | this._posY = scrollY |
| 198 | if (!this._scrolling) { |
| 199 | requestAnimationFrame(() => this._avanza()) |
| 200 | } |
| 201 | this._scrolling = true |
| 202 | } |
| 203 | |
| 204 | /** @private */ |
| 205 | _avanza() { |
| 206 | if (this._posY === 0) { |
| 207 | this.classList.remove("scroll") |
| 208 | if (this._headline !== null) { |
| 209 | if (this._adicional === null) { |
| 210 | this._headline.classList.remove("scroll") |
| 211 | } else { |
| 212 | this._headline.classList.remove("scroll-adicional") |
| 213 | } |
| 214 | } |
| 215 | if (this._adicional !== null) { |
| 216 | this._adicional.classList.remove("scroll") |
| 217 | } |
| 218 | } else { |
| 219 | this.classList.add("scroll") |
| 220 | if (this._headline !== null) { |
| 221 | if (this._adicional === null) { |
| 222 | this._headline.classList.add("scroll") |
| 223 | } else { |
| 224 | this._headline.classList.add("scroll-adicional") |
| 225 | } |
| 226 | } |
| 227 | if (this._adicional !== null) { |
| 228 | this._adicional.classList.add("scroll") |
| 229 | } |
| 230 | } |
| 231 | this._scrolling = false |
| 232 | } |
| 233 | |
| 234 | } |
| 235 | |
| 236 | customElements.define("md-app-bar", MdAppBar) |