C. Carpeta « lib / js »

Versión para imprimir.

1. lib / js / abreElementoHtml.js

1/**
2 * @param { HTMLElement } elementoHtml
3 */
4export function abreElementoHtml(elementoHtml) {
5 const list = elementoHtml.classList
6 if (!list.contains("open")) {
7 elementoHtml.hidden = false
8 setTimeout(() => list.add("open"), 100)
9 }
10}

2. lib / js / cierraElementoHtmo.js

1/**
2 * @param { HTMLElement } elementoHtml
3 */
4export function cierraElementoHtmo(elementoHtml) {
5 const list = elementoHtml.classList
6 if (list.contains("open")) {
7 list.remove("open")
8 setTimeout(() => elementoHtml.hidden = true, 500)
9 }
10}
11

3. lib / js / getAttribute.js

1/**
2 * @param {HTMLElement} elementoHtml
3 * @param {string} nombre
4 * @returns {string}
5 */
6export function getAttribute(elementoHtml, nombre) {
7 const valor = elementoHtml.getAttribute(nombre)
8 return valor === null ? "" : valor
9}

4. lib / js / htmlentities.js

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 * @returns { string } un texto que no puede
8 * interpretarse como HTML. */
9export function htmlentities(texto) {
10 return texto.replace(/[<>"']/g, textoDetectado => {
11 switch (textoDetectado) {
12 case "<": return "<"
13 case ">": return ">"
14 case '"': return """
15 case "'": return "'"
16 default: return textoDetectado
17 }
18 })
19}

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

6. lib / js / muestraTextoDeAyuda.js

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 estado de
12 * elementoHtml es válido.
13 */
14export function muestraTextoDeAyuda(elementoHtml, elementoDeAyuda,
15 mensajeDeAyuda) {
16 if (elementoHtml.validity.valid) {
17 elementoDeAyuda.textContent = mensajeDeAyuda
18 } else {
19 elementoDeAyuda.textContent = elementoHtml.validationMessage
20 }
21}
22
23// Permite que los eventos de html usen la función.
24window["muestraTextoDeAyuda"] = muestraTextoDeAyuda

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

8. lib / js / querySelector.js

1/**
2 * @template { HTMLElement } T
3 * @param { Document | Element | ShadowRoot } raiz
4 * @param { string } query
5 * @returns { T }
6 */
7export 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}

9. lib / js / registraServiceWorkerSiEsSoportado.js

1import { muestraError } from "./muestraError.js"
2
3/**
4 * @param { string | URL } urlDeServiceWorker
5 */
6export async function registraServiceWorkerSiEsSoportado(urlDeServiceWorker) {
7 try {
8 if ("serviceWorker" in navigator) {
9 const registro = await navigator.serviceWorker.register(urlDeServiceWorker)
10 console.log(urlDeServiceWorker, "registrado.")
11 console.log(registro)
12 }
13 } catch (error) {
14 muestraError(error)
15 }
16}

10. lib / js / resaltaSiEstasEn.js

1
2/**
3 * @param {string[]} paginas
4 */
5export function resaltaSiEstasEn(paginas) {
6 const pathname = location.pathname
7 return paginas.includes(pathname) ? `class="active"` : ""
8}

11. Carpeta « lib / js / const »

Versión para imprimir.

A. lib / js / const / ES_APPLE.js

1export const ES_APPLE = /.*(iPad|iPhone|iPod|Mac).*/.test(navigator.userAgent)

12. Carpeta « lib / js / custom »

Versión para imprimir.

A. lib / js / custom / md-menu-button.js

1export class MdMenuButton extends HTMLButtonElement {
2
3 connectedCallback() {
4 this.type = "button"
5 this.classList.add("md-standard-icon-button")
6 this.innerHTML = /* HTML */
7 `<span class="material-symbols-outlined">menu</span>`
8 }
9
10}
11
12customElements.define("md-menu-button", MdMenuButton, { extends: "button" })

B. lib / js / custom / md-options-menu.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3import { querySelector } from "../querySelector.js"
4
5export class MdOptionsMenu extends HTMLElement {
6
7 getContent() {
8 return /* HTML */`
9
10 <style>
11
12 :host {
13 position: absolute;
14 }
15
16 </style>
17
18 <slot></slot>`
19 }
20
21 constructor() {
22 super()
23 const shadow = this.attachShadow({ mode: "open" })
24 shadow.innerHTML = this.getContent()
25 this._configuraOpciones = this._configuraOpciones.bind(this)
26
27 /**
28 * @private
29 * @type { HTMLSlotElement }
30 */
31 this._slot = querySelector(shadow, "slot")
32 /**
33 * @private
34 * @type { HTMLElement[] }
35 */
36 this._opciones = []
37 this._slot.addEventListener("slotchange", this._configuraOpciones)
38 }
39
40 connectedCallback() {
41 this.classList.add("md-menu")
42 this.hidden = true
43 this.role = "listbox"
44 }
45
46 /**
47 * @returns {readonly Readonly<HTMLElement>[]}
48 */
49 get opciones() {
50 return this._opciones
51 }
52
53 get seleccion() {
54 /** @type { HTMLInputElement | null } */
55 const seleccionado = this.querySelector(".selected")
56 return seleccionado === null ? "" : seleccionado.value
57 }
58
59 _configuraOpciones() {
60 /**
61 * @type {HTMLElement[]}
62 */
63 const opciones = []
64 for (const opcion of this._slot.assignedElements()) {
65 opcion.role = "option"
66 if (opcion instanceof HTMLElement) {
67 opciones.push(opcion)
68 }
69 }
70 this._opciones = opciones
71 }
72
73 abre() {
74 abreElementoHtml(this)
75 }
76
77
78 cierra() {
79 cierraElementoHtmo(this)
80 }
81
82 /**
83 * @param {string} value
84 */
85 muestraValue(value) {
86 let texto = ""
87 for (const opcion of this._opciones) {
88 if (opcion.dataset.value === value) {
89 opcion.classList.add("selected")
90 let textContent = opcion.textContent
91 if (texto === "" && textContent !== null) {
92 textContent = textContent.trim()
93 if (textContent !== "") {
94 texto = textContent
95 }
96 }
97 } else {
98 opcion.classList.remove("selected")
99 }
100 }
101 return texto
102 }
103
104}
105
106customElements.define("md-options-menu", MdOptionsMenu)

C. lib / js / custom / md-overflow-button.js

1import { ES_APPLE } from "../const/ES_APPLE.js"
2
3export class MdOverflowButton extends HTMLButtonElement {
4
5 connectedCallback() {
6 this.type = "button"
7 this.classList.add("md-standard-icon-button")
8 this.innerHTML = ES_APPLE
9 ? /* HTML */
10 `<span style="color: var(--md-sys-color-on-surface-variant)"
11 class="material-symbols-outlined">
12 more_horiz
13 </span>`
14 : /* HTML */
15 `<span style="color: var(--md-sys-color-on-surface-variant)"
16 class="material-symbols-outlined">
17 more_vert
18 </span>`
19 }
20
21}
22
23customElements
24 .define("md-overflow-button", MdOverflowButton, { extends: "button" })

D. lib / js / custom / md-overflow-menu.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3
4export class MdOverflowMenu extends HTMLElement {
5
6 getContent() {
7 return /* HTML */`
8
9 <style>
10
11 :host {
12 position: fixed;
13 min-width: 7rem;
14 max-width: 280px;
15 }
16
17 ::slotted(*) {
18 text-align: start;
19 width: 100%;
20 border: none;
21 background-color: transparent;
22 }
23
24 </style>
25
26 <slot></slot>`
27 }
28
29 constructor() {
30 super()
31 const shadow = this.attachShadow({ mode: "open" })
32 shadow.innerHTML = this.getContent()
33 this.clicCierra = this.clicCierra.bind(this)
34 /**
35 * @private
36 * @type {HTMLButtonElement| null}
37 */
38 this._toggleButton = null
39 }
40
41 connectedCallback() {
42 this.classList.add("md-menu")
43 this.hidden = true
44 this.role = "menu"
45 }
46
47 /**
48 * @param {HTMLButtonElement} toggleButton
49 */
50 alterna(toggleButton) {
51 this._toggleButton = toggleButton
52 const top = toggleButton.offsetTop + toggleButton.offsetHeight - 4
53 const right =
54 innerWidth - (toggleButton.offsetLeft + toggleButton.offsetWidth) - 3
55 this.style.top = `${top}px`
56 this.style.right = `${right}px`
57 const list = this.classList
58 if (list.contains("open")) {
59 this.cierra()
60 } else {
61 this.abre()
62 }
63 }
64
65 abre() {
66 document.addEventListener("click", this.clicCierra)
67 abreElementoHtml(this)
68 }
69
70 cierra() {
71 document.removeEventListener("click", this.clicCierra)
72 cierraElementoHtmo(this)
73 }
74
75 /**
76 * @param {Event} evt
77 */
78 clicCierra(evt) {
79 const target = evt.target
80 if (this.classList.contains("open")
81 && this._toggleButton !== null
82 && target instanceof HTMLElement
83 && !this._toggleButton.contains(target)) {
84 this.cierra()
85 }
86 }
87}
88
89customElements.define("md-overflow-menu", MdOverflowMenu)

E. lib / js / custom / md-select-menu.js

1import { getAttribute } from "../getAttribute.js"
2import { querySelector } from "../querySelector.js"
3import { MdOptionsMenu } from "./md-options-menu.js"
4
5export class MdSelectMenu extends HTMLElement {
6
7 static get observedAttributes() {
8 return ["options", "value", "required"]
9 }
10
11 getContent() {
12 return /* HTML */ `
13 <link rel="stylesheet" href="/lib/css/material-symbols-outlined.css">
14
15 <style>
16 :host {
17 display: block;
18 cursor: default;
19 }
20
21 output {
22 display: block;
23 padding-right: 2rem;
24 white-space: nowrap;
25 text-overflow: ellipsis;
26 overflow: hidden;
27 }
28
29 #up {
30 position: absolute;
31 bottom: 0.5rem;
32 right: 0.75rem;
33 display: none;
34 color: var(--md-sys-color-on-surface-variant);
35 }
36
37 #down {
38 position: absolute;
39 bottom: 0.5rem;
40 right: 0.75rem;
41 color: var(--md-sys-color-on-surface-variant);
42 }
43
44 :host(.open) #up {
45 display: inline-block;
46 }
47
48 :host(.open) #down {
49 display: none;
50 }
51
52 :host(:invalid) #up,
53 :host(:invalid) #down {
54 color: var(--md-sys-color-error);
55 }
56
57 </style>
58 <output></output>
59 <span id="down" class="material-symbols-outlined">
60 arrow_drop_down
61 </span>
62 <span id="up" class="material-symbols-outlined">
63 arrow_drop_up
64 </span>`
65 }
66
67 constructor() {
68 super()
69
70 const shadow = this.attachShadow({ mode: "open" })
71 shadow.innerHTML = this.getContent()
72
73 this._alterna = this._alterna.bind(this)
74 this._onKeyDown = this._onKeyDown.bind(this)
75 this._cierra = this._cierra.bind(this)
76 this._clicEnDialogo = this._clicEnDialogo.bind(this)
77 this.clicExterno = this.clicExterno.bind(this)
78 this.muestraValue = this.muestraValue.bind(this)
79
80 /**
81 * @private
82 * @type {string}
83 */
84 this._customValidity = ""
85
86 /**
87 * @private
88 * @type { HTMLOutputElement }
89 */
90 this.output = querySelector(shadow, "output")
91 /**
92 * @private
93 * @type { MdOptionsMenu | null }
94 */
95 this._optionsMenu = null
96 /**
97 * @protected
98 * @readonly
99 */
100 this._internals = this.attachInternals()
101 this._internals.role = "select"
102 addEventListener("load", this.muestraValue)
103 }
104
105 connectedCallback() {
106 this.tabIndex = 0
107 this.role = "combobox"
108 this.ariaHasPopup = "listbox"
109 this.ariaExpanded = "false"
110 this["aria-controls"] = this.options
111 this.addEventListener("keydown", this._onKeyDown)
112 const parentElement = this.parentElement
113 if (parentElement !== null) {
114 parentElement.addEventListener("click", this._alterna)
115 }
116 }
117
118 /**
119 * @param {string} nombreDeAtributo
120 * @param {string} _valorAnterior
121 * @param {string} _nuevoValor
122 */
123 attributeChangedCallback(nombreDeAtributo, _valorAnterior, _nuevoValor) {
124 switch (nombreDeAtributo) {
125 case "options":
126 this._cambiaOptions()
127 break
128 case "value":
129 this.muestraValue()
130 break
131 case "required":
132 this.checkValidity()
133 break
134 }
135 }
136
137 get options() {
138 return getAttribute(this, "options")
139 }
140
141 set options(options) {
142 this.setAttribute("options", options)
143 }
144
145 _cambiaOptions() {
146 if (this._optionsMenu !== null) {
147 this._optionsMenu = null
148 }
149 this["aria-controls"] = this.options
150 }
151
152 get required() {
153 return this.hasAttribute("required")
154 }
155
156 set required(required) {
157 this.toggleAttribute("required", Boolean(required))
158 }
159
160 get value() {
161 return getAttribute(this, "value")
162 }
163
164 set value(value) {
165 this.setAttribute("value", value)
166 }
167
168 get name() {
169 return getAttribute(this, "name")
170 }
171
172 set name(name) {
173 this.setAttribute("name", name)
174 }
175
176 muestraValue() {
177 const value = this.value
178 this._internals.setFormValue(value)
179
180 // En un futuro se usará esto en vez de la clase populated.
181 // if (value === "") {
182 // this._internals.states.delete("populated")
183 // } else {
184 // this._internals.states.add("populated")
185 // }
186
187 if (this.isConnected) {
188 if (value === "") {
189 this.classList.remove("populated")
190 } else {
191 this.classList.add("populated")
192 }
193 this._checkValidity()
194 const optionsMenu = this.optionsMenu
195 if (optionsMenu !== null) {
196 this.output.value = optionsMenu.muestraValue(value)
197 }
198 }
199 }
200
201 get form() {
202 return this._internals && this._internals.form
203 }
204
205 get willValidate() {
206 return this._internals ? this._internals.willValidate : true
207 }
208
209 /**
210 * @param {string} message
211 */
212 setCustomValidity(message) {
213 this._customValidity = message
214 this._checkValidity()
215 }
216
217 /**
218 * @returns {ValidityState}
219 */
220 get validity() {
221 return this._internals.validity
222 }
223
224 checkValidity() {
225 return this._internals.checkValidity()
226 }
227
228 reportValidity() {
229 return this._internals.reportValidity()
230 }
231
232 get validationMessage() {
233 return this._internals.validationMessage
234 }
235 /** @returns {boolean} */
236 _checkValidity() {
237 if (this._customValidity !== "") {
238 this._internals.setValidity({ customError: true }, this._customValidity)
239 return false
240 } else if (this.required && this.value === "") {
241 this._internals.setValidity({ valueMissing: true }, "Seleccione una opción.")
242 return false
243 } else {
244 this._internals.setValidity({})
245 return true
246 }
247 }
248
249 /** @private */
250 _alterna() {
251 if (this.classList.contains("open")) {
252 this._cierra()
253 } else {
254 this._abre()
255 }
256 }
257
258 /** @private */
259 _abre() {
260 this.classList.add("open")
261 const parentElement = this.parentElement
262 if (parentElement !== null) {
263 const optionsMenu = this.optionsMenu
264 if (optionsMenu !== null) {
265 optionsMenu.style.top = `${parentElement.offsetTop + 58}px`
266 optionsMenu.style.left = `${parentElement.offsetLeft}px`
267 optionsMenu.style.width = `${parentElement.offsetWidth}px`
268 optionsMenu.abre()
269 this.focus()
270 optionsMenu.addEventListener("click", this._clicEnDialogo)
271 }
272 this.ariaExpanded = "true"
273 document.addEventListener("click", this.clicExterno)
274 }
275 }
276
277 /** @private */
278 _cierra() {
279 this.classList.remove("open")
280 const optionsMenu = this.optionsMenu
281 if (optionsMenu !== null) {
282 optionsMenu.cierra()
283 optionsMenu.removeEventListener("click", this._clicEnDialogo)
284 }
285 this.ariaExpanded = "false"
286 document.removeEventListener("click", this.clicExterno)
287 this.dispatchEvent(new Event("input", { bubbles: true }))
288 }
289
290 get optionsMenu() {
291 if (this._optionsMenu === null) {
292 if (this.options !== "") {
293 const optionsMenu = document.getElementById(this.options)
294 if (optionsMenu instanceof MdOptionsMenu) {
295 this._optionsMenu = optionsMenu
296 } else {
297 throw new Error(`Valor incorrecto para options: "${this.options}".`)
298 }
299 }
300 }
301 return this._optionsMenu
302 }
303
304 /** @private */
305 _avanzaOpcion() {
306 const i = this._valueIndex
307 if (i > -1) {
308 const optionsMenu = this.optionsMenu
309 if (optionsMenu !== null) {
310 const opciones = optionsMenu.opciones
311 if (i < opciones.length - 1) {
312 this.value = getAttribute(opciones[i + 1], "data-value")
313 }
314 }
315 }
316 }
317
318 /** @private */
319 _retrocedeOpcion() {
320 const i = this._valueIndex
321 if (i > -1) {
322 const optionsMenu = this.optionsMenu
323 if (optionsMenu !== null) {
324 const opciones = optionsMenu.opciones
325 if (i > 0) {
326 this.value = getAttribute(opciones[i - 1], "data-value")
327 }
328 }
329 }
330 }
331
332 /**
333 * @private
334 * @returns {number}
335 */
336 get _valueIndex() {
337 const value = this.value
338 const optionsMenu = this.optionsMenu
339 return (optionsMenu === null
340 ? -1
341 : optionsMenu.opciones.findIndex(opcion => opcion.dataset.value === value))
342 }
343
344 /**
345 * @private
346 * @param {Event} event
347 */
348 _clicEnDialogo(event) {
349 const target = event.target
350 const optionsMenu = this.optionsMenu
351 let value = ""
352 if (optionsMenu !== null) {
353 for (const opcion of optionsMenu.opciones) {
354 if (opcion === target) {
355 opcion.classList.add("selected")
356 value = getAttribute(opcion, "data-value")
357 } else {
358 opcion.classList.remove("selected")
359 }
360 }
361 }
362 this.value = value
363 this._cierra()
364 this.focus()
365}
366
367 /**
368 * @param {Event} evt
369 */
370 clicExterno(evt) {
371 const target = evt.target
372 const parentElement = this.parentElement
373 const optionsMenu = this._optionsMenu
374 if (this.classList.contains("open")
375 && target instanceof HTMLElement
376 && parentElement !== null
377 && !parentElement.contains(target)
378 && optionsMenu !== null
379 && !optionsMenu.contains(target)) {
380 this._cierra()
381 }
382 }
383
384 /**
385 * @param { KeyboardEvent } event
386 */
387 _onKeyDown(event) {
388 const key = event.key
389 const optionsMenu = this._optionsMenu
390 if (optionsMenu !== null) {
391 if (optionsMenu.classList.contains("open")) {
392 if (key === "ArrowDown") {
393 event.preventDefault()
394 this._avanzaOpcion()
395 } else if (key === "ArrowUp") {
396 event.preventDefault()
397 this._retrocedeOpcion()
398 } else if (key === "Escape") {
399 event.preventDefault()
400 this._cierra()
401 } else if (key === " ") {
402 event.preventDefault()
403 this._cierra()
404 } else if (key === "Tab") {
405 this._cierra()
406 } else {
407 event.preventDefault()
408 }
409 } else if (key === " ") {
410 event.preventDefault()
411 this._abre()
412 } else if (key === "Tab") {
413 this._cierra()
414 } else {
415 event.preventDefault()
416 }
417 }
418 }
419
420}
421
422MdSelectMenu.formAssociated = true
423
424customElements.define("md-select-menu", MdSelectMenu)

F. lib / js / custom / md-slider-field.js

1import { querySelector } from "../querySelector.js"
2
3export class MdSliderField extends HTMLElement {
4
5 getContent() {
6 return /* HTML */`
7 <style>
8 :host {
9 display: block;
10 margin: 1rem;
11 }
12
13 :host([hidden]) {
14 display: none;
15 }
16
17 #label::slotted(*) {
18 display: block;
19 white-space: nowrap;
20 text-overflow: ellipsis;
21 overflow: hidden;
22 color: var(--md-sys-color-on-surface-variant);
23 font-family: var(--md-sys-typescale-body-small-font);
24 font-weight: var(--md-sys-typescale-body-small-weight);
25 font-size: var(--md-sys-typescale-body-small-size);
26 font-style: var(--md-sys-typescale-body-small-font-style);
27 letter-spacing: var(--md-sys-typescale-body-small-tracking);
28 line-height: var(--md-sys-typescale-body-small-line-height);
29 text-transform: var(--md-sys-typescale-body-small-text-transform);
30 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
31 }
32
33 [name="slider"]::slotted(input) {
34 -webkit-appearance: none;
35 appearance: none;
36 height: 0.25rem;
37 border-radius: 0.125rem;
38 background-image:
39 linear-gradient(to right, var(--md-sys-color-primary) 0%, var(--md-sys-color-primary) 50%, var(--md-sys-color-surface-container-highest) 50%, var(--md-sys-color-surface-container-highest) 100%);
40 }
41
42 [name="slider"]::slotted(input:focus) {
43 outline: none;
44 }
45
46 [name="supporting"]::slotted(*) {
47 display: block;
48 color: var(--md-sys-color-on-surface-variant);
49 font-family: var(--md-sys-typescale-body-small-font);
50 font-weight: var(--md-sys-typescale-body-small-weight);
51 font-size: var(--md-sys-typescale-body-small-size);
52 font-style: var(--md-sys-typescale-body-small-font-style);
53 letter-spacing: var(--md-sys-typescale-body-small-tracking);
54 line-height: var(--md-sys-typescale-body-small-line-height);
55 text-transform: var(--md-sys-typescale-body-small-text-transform);
56 text-decoration: var(--md-sys-typescale-body-small-text-decoration);
57 padding-top: 0.5rem;
58 white-space: nowrap;
59 text-overflow: ellipsis;
60 overflow: hidden;
61 }
62 </style>
63 <slot id="label"></slot>
64 <slot name="slider"></slot>
65 <slot name="supporting"></slot>`
66 }
67
68 constructor() {
69 super()
70 const shadow = this.attachShadow({ mode: "open", delegatesFocus: true })
71 shadow.innerHTML = this.getContent()
72 this._configuraSlider = this._configuraSlider.bind(this)
73 this.analiza = this.analiza.bind(this)
74
75 /**
76 * @private
77 * @type {HTMLSlotElement}
78 */
79 this._slotSlider = querySelector(shadow, '[name="slider"]')
80 /**
81 * @private
82 * @type {HTMLInputElement|null}
83 */
84 this._input = null
85 this._slotSlider.addEventListener("slotchange", this._configuraSlider)
86 }
87
88 /** @private */
89 _configuraSlider() {
90 if (this._input !== null) {
91 this._input.removeEventListener("input", this.analiza)
92 this._input = null
93 }
94 for (const input of this._slotSlider.assignedElements()) {
95 if (input instanceof HTMLInputElement) {
96 this._input = input
97 input.addEventListener("input", this.analiza)
98 this.analiza()
99 }
100 }
101 }
102
103 analiza() {
104 const i = this._input
105 if (i !== null) {
106 const v = i.valueAsNumber
107 const min = parseFloat(i.min)
108 const max = parseFloat(i.max)
109 const value = (v - min) / (max - min) * 100
110 i.title = v.toString()
111 i.style.background =
112 `linear-gradient(to right, var(--md-sys-color-primary) 0%, var(--md-sys-color-primary) ${value
113 }%, var(--md-sys-color-surface-container-highest) ${value
114 }%, var(--md-sys-color-surface-container-highest) 100%)`
115 }
116 }
117
118}
119
120customElements.define("md-slider-field", MdSliderField)

G. lib / js / custom / md-top-app-bar.js

1import { ES_APPLE } from "../const/ES_APPLE.js"
2import { getAttribute } from "../getAttribute.js"
3import { querySelector } from "../querySelector.js"
4
5class MdTopAppBar 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(.centrado) #acciones,
49 :host(.center-aligned) #acciones {
50 flex: 0 0 3rem;
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 transition-property: opacity;
70 transition-duration: var(--md-sys-motion-duration-700);
71 }
72
73 :host(.headline) #headline::slotted(*) {
74 opacity: 0;
75 }
76
77 :host(.scroll.headline) #headline::slotted(*) {
78 opacity: 1;
79 }
80
81 :host(.center-aligned) #headline::slotted(*) {
82 flex: 1 1 auto;
83 text-align: center
84 }
85
86 </style>
87
88 <span id="navigation">
89 <slot name="navigation"></slot>
90 </span>
91 <slot id="headline"></slot>
92 <span id="acciones">
93 <slot name="action"></slot>
94 </span>`
95 }
96
97 constructor() {
98 super()
99 if (ES_APPLE) {
100 document.body.classList.add("apple")
101 document.body.classList.remove("material")
102 } else {
103 document.body.classList.add("material")
104 document.body.classList.remove("apple")
105 }
106
107 /**
108 * @private
109 * @readonly
110 */
111 const shadow = this.attachShadow({ mode: "open" })
112 shadow.innerHTML = this.getContent()
113 this._configuraAction = this._configuraAction.bind(this)
114 /**
115 * @private
116 * @type {number}
117 */
118 this._posY = 0
119 /**
120 * @private
121 * @type {boolean}
122 */
123 this._scrolling = false
124 /**
125 * @private
126 * @type { HTMLSlotElement }
127 */
128 this._navigation = querySelector(shadow, '[name="navigation"]')
129 /**
130 * @private
131 * @type { HTMLSlotElement }
132 */
133 this._action = querySelector(shadow, '[name="action"]')
134 /**
135 * @private
136 * @type { HTMLHeadingElement | null }
137 */
138 this._headline = null
139 /**
140 * @private
141 * @type { HTMLElement | null }
142 */
143 this._adicional = null
144 this._action.addEventListener("slotchange", this._configuraAction)
145 addEventListener("scroll", () => this._onScroll())
146 addEventListener("load", () => this.configurOtros())
147 }
148
149 connectedCallback() {
150 this.role = "toolbar"
151 this._configuraAction()
152 }
153
154 configurOtros() {
155 const idHeadline = getAttribute(this, "headline")
156 if (idHeadline !== "") {
157 const headline = document.getElementById(idHeadline)
158 if (headline instanceof HTMLHeadingElement) {
159 this._headline = headline
160 this.classList.add("headline")
161 if (this.classList.contains("apple") || this.classList.contains("medium")) {
162 headline.classList.add("md-headline", "headline-small")
163 } else {
164 headline.classList.add("md-headline", "headline-medium")
165 }
166 }
167 }
168 const idAdicional = getAttribute(this, "adicional")
169 if (idAdicional !== "") {
170 this._adicional = document.getElementById(idAdicional)
171 if (this._adicional !== null) {
172 if (this.classList.contains("apple")) {
173 this._adicional.style.top = "env(titlebar-area-height, 3rem)"
174 } else {
175 this._adicional.style.top = "env(titlebar-area-height, 4rem)"
176 }
177 }
178 }
179 }
180
181 _configuraAction() {
182 const assignedElements = this._action.assignedElements()
183 if (this.isConnected) {
184 if (ES_APPLE) {
185 this.classList.add("apple")
186 this.classList.remove("material")
187 } else {
188 this.classList.add("material")
189 this.classList.remove("apple")
190 }
191 if (this.classList.contains("center-aligned")) {
192 this.classList.remove("centrado")
193 this.classList.remove("justificado")
194 } else {
195 if (ES_APPLE && assignedElements.length <= 1) {
196 this.classList.add("centrado")
197 this.classList.remove("justificado")
198 } else {
199 this.classList.add("justificado")
200 this.classList.remove("centrado")
201 }
202 }
203 }
204 }
205
206 /** @private */
207 _onScroll() {
208 this._posY = scrollY
209 if (!this._scrolling) {
210 requestAnimationFrame(() => this._avanza())
211 }
212 this._scrolling = true
213 }
214
215 /** @private */
216 _avanza() {
217 if (this._posY === 0) {
218 this.classList.remove("scroll")
219 if (this._headline !== null) {
220 if (this._adicional === null) {
221 this._headline.classList.remove("scroll")
222 } else {
223 this._headline.classList.remove("scroll-adicional")
224 }
225 }
226 if (this._adicional !== null) {
227 this._adicional.classList.remove("scroll")
228 }
229 } else {
230 this.classList.add("scroll")
231 if (this._headline !== null) {
232 if (this._adicional === null) {
233 this._headline.classList.add("scroll")
234 } else {
235 this._headline.classList.add("scroll-adicional")
236 }
237 }
238 if (this._adicional !== null) {
239 this._adicional.classList.add("scroll")
240 }
241 }
242 this._scrolling = false
243 }
244
245}
246
247customElements.define("md-top-app-bar", MdTopAppBar)

H. lib / js / custom / MdNavigationDrawer.js

1import { abreElementoHtml } from "../abreElementoHtml.js"
2import { cierraElementoHtmo } from "../cierraElementoHtmo.js"
3import { querySelector } from "../querySelector.js"
4
5export class MdNavigationDrawer extends HTMLElement {
6
7 /**
8 * @returns {string}
9 */
10 getHipervinculos() { throw new Error("abstract") }
11
12 getContent() {
13 return /* HTML */`
14
15 <link rel="stylesheet" href="/lib/css/material-symbols-outlined.css">
16 <link rel="stylesheet" href="/lib/css/shape.css">
17 <link rel="stylesheet" href="/lib/css/md-ripple.css">
18
19 <style>
20
21 nav {
22 display: flex;
23 flex-direction: column;
24 position: fixed;
25 z-index: 4;
26 box-sizing: border-box;
27 top: 0;
28 left: 0;
29 bottom: 0;
30 width: var(--anchoNav);
31 max-width: 80vw;
32 overflow: hidden;
33 transform: translateX(-100%);
34 background-color: var(--md-sys-color-surface-container-low);
35 transition-property: transform;
36 transition-duration: var(--md-sys-motion-duration-700);
37 }
38
39 :host(.open) nav {
40 transform: translateX(0);
41 }
42
43 nav>div {
44 flex-grow: 1;
45 overflow: auto;
46 padding: 0.75rem 1rem;
47 }
48
49 h1 {
50 margin: 0;
51 height: 3.5rem;
52 line-height: 3.5rem;
53 padding: 0 0 0 0.75rem;
54 white-space: nowrap;
55 text-overflow: ellipsis;
56 overflow: hidden;
57 color: var(--md-sys-color-on-surface-variant);
58 font-family: var(--md-sys-typescale-title-small-font);
59 font-weight: var(--md-sys-typescale-title-small-weight);
60 font-size: var(--md-sys-typescale-title-small-size);
61 font-style: var(--md-sys-typescale-title-small-font-style);
62 letter-spacing: var(--md-sys-typescale-title-small-tracking);
63 text-transform: var(--md-sys-typescale-title-small-text-transform);
64 text-decoration: var(--md-sys-typescale-title-small-text-decoration);
65 }
66
67 a::after { /* container inactive */
68 content: "";
69 position: absolute;
70 z-index: -2;
71 top: 0;
72 right: 0;
73 left: 0;
74 bottom: 0;
75 }
76
77 a.active::after { /* container */
78 background-color: var(--md-sys-color-secondary-container);
79 }
80
81 a { /* label, shape inactive */
82 position: relative;
83 display: block;
84 box-sizing: border-box;
85 height: 3.5rem;
86 line-height: 3.5rem;
87 padding: 0 0.75rem;
88 border-radius: 1.75rem;
89 color: var(--md-sys-color-on-surface-variant);
90 font-family: var(--md-sys-typescale-label-large-font);
91 font-weight: var(--md-sys-typescale-label-large-weight);
92 font-size: var(--md-sys-typescale-label-large-size);
93 font-style: var(--md-sys-typescale-label-large-font-style);
94 letter-spacing: var(--md-sys-typescale-label-large-tracking);
95 text-transform: var(--md-sys-typescale-label-large-text-transform);
96 text-decoration: var(--md-sys-typescale-label-large-text-decoration);
97 overflow: hidden;
98 white-space: nowrap;
99 text-overflow: ellipsis;
100 }
101
102 a.active { /* label, shape */
103 font-weight: var(--md-sys-typescale-label-large-weight-prominent);
104 color: var(--md-sys-color-on-secondary-container);
105 }
106
107 a::before { /* state layer */
108 content: "";
109 position: absolute;
110 z-index: -1;
111 top: 0;
112 right: 0;
113 left: 0;
114 bottom: 0;
115 }
116
117 a span { /* inactive icon */
118 position: relative;
119 margin-right: 0.75rem;
120 vertical-align: middle;
121 color: var(--md-sys-color-on-surface-variant);
122 font-size: 1.5rem;
123 width: 1.5rem;
124 height: 1.5rem;
125 }
126
127 a.active span { /* icon */
128 color: var(--md-sys-color-on-secondary-container);
129 }
130
131 #scrim {
132 position: fixed;
133 z-index: 3;
134 top: 0;
135 left: 0;
136 bottom: 0;
137 right: 0;
138 background-color: var(--md-ref-palette-neutral-variant20);
139 opacity: 0.4;
140 transform: translateX(-100%);
141 transition-property: transform;
142 animation-duration: var(--md-sys-motion-duration-700);
143 }
144
145 :host(.open) #scrim {
146 transform: translateX(0);
147 }
148
149 a:hover { /* inactive label, shape */
150 color: var(--md-sys-color-on-surface);
151 }
152
153 a.active:hover { /* active label, shape */
154 color: var(--md-sys-color-on-secondary-container);
155 }
156
157 a:hover::before { /* inactive state layer */
158 background-color: var(--md-sys-color-on-surface);
159 opacity: var(--md-sys-state-hover-state-layer-opacity);
160 }
161
162 a.active:hover::before { /* state layer */
163 background-color: var(--md-sys-color-on-secondary-container);
164 }
165
166 a:hover span { /* inactive icon */
167 color: var(--md-sys-color-on-surface);
168 }
169
170 a.active:hover span { /* icon */
171 color: var(--md-sys-color-on-secondary-container);
172 }
173
174 a:focus { /* inactive label, shape */
175 outline: none;
176 color: var(--md-sys-color-on-surface);
177 }
178
179 a.active:focus { /* label, shape */
180 color: var(--md-sys-color-on-secondary-container);
181 }
182
183 a:focus::before { /* inactive state layer */
184 background-color: var(--md-sys-color-on-surface);
185 opacity: var(--md-sys-state-focus-state-layer-opacity);
186 }
187
188 a.active:focus::before { /* state layer */
189 background-color: var(--md-sys-color-on-secondary-container);
190 }
191
192 a:focus span { /* inactive icon */
193 color: var(--md-sys-color-on-surface);
194 }
195
196 a.active:focus span { /* icon */
197 color: var(--md-sys-color-on-secondary-container);
198 }
199
200
201 a:active { /* inactive pressed label, shape */
202 background-position: center;
203 background-image:
204 radial-gradient(circle, var(--md-riple-color) 1%, transparent 1%);
205 background-size: 100%;
206 animation-name: md-ripple;
207 animation-duration: var(--md-sys-motion-duration-500);
208 color: var(--md-sys-color-on-surface);
209 }
210
211 a.active:active { /* active pressed label, shape */
212 color: var(--md-sys-color-on-secondary-container);
213 }
214
215 a:active::before { /* inactive pressed state layer */
216 background-color: var(--md-sys-color-on-surface);
217 opacity: var(--md-sys-state-pressed-state-layer-opacity);
218 }
219
220 a.active:active::before { /* active pressed state layer */
221 background-color: var(--md-sys-color-on-secondary-container);
222 }
223
224 a:active span { /* inactive pressed icon */
225 color: var(--md-sys-color-on-surface);
226 }
227
228 a.active:focus span { /* active pressed icon */
229 color: var(--md-sys-color-on-secondary-container);
230 }
231
232 </style>
233
234 <div id="scrim"></div>
235 <nav class="large-end"><div></div></nav>`
236 }
237
238 constructor() {
239 super()
240 const shadow = this.attachShadow({ mode: "open", delegatesFocus: true })
241 shadow.innerHTML = this.getContent()
242 this.cierra = this.cierra.bind(this)
243
244 /** @type {HTMLUListElement} */
245 this._div = querySelector(shadow, "nav>div")
246
247 /** @type {HTMLUListElement} */
248 this._scrim = querySelector(shadow, "#scrim")
249 this._scrim.addEventListener("click", this.cierra)
250 }
251
252 connectedCallback() {
253 this.hidden = true
254 this._div.innerHTML = this.getHipervinculos()
255 }
256
257 abre() {
258 abreElementoHtml(this)
259 }
260
261 cierra() {
262 cierraElementoHtmo(this)
263 }
264
265}