Hacer un Polyfill completo para el elemento de detalles HTML5

 

 

 


Índice
  1. Las soluciones existentes están incompletas
  2. Soporte de contenido futuro
  3. Implementación del summarycomportamiento
  4. summaryCasos de borde de elementos
    1. 1. ¿Cuándo summaryes un niño pero no el primer hijo?
    2. 2. Cuando el summaryelemento no está presente
  5. Apoyo a openla propiedad
  6. Fix For Infinite Recursion In IE 8
  7. Polyfill For The open Attribute
  8. Implementación final
  9. Notas sobre el estilo
  10. Poniendolo todo junto

Desarrollar un polyfill no es el desafío más fácil. Por otro lado, la solución se puede utilizar durante un tiempo relativamente largo: los estándares no cambian con frecuencia y se han discutido extensamente entre bastidores. Además, todos usan el mismo idioma y se conectan con las mismas API, lo cual es genial. Este es un artículo bastante técnico y, aunque Maksim Chemerisuk intentará minimizar los fragmentos de código, este artículo todavía contiene bastantes de ellos. ¡Asi que preparate!

 

HTML5 introdujo un montón de etiquetas nuevas, una de las cuales es details. Este elemento es una solución para un componente común de la interfaz de usuario: un bloque plegable. Casi todos los marcos, incluidos Bootstrap y jQuery UI, tienen su propio complemento para una solución similar, pero ninguno se ajusta a la especificación HTML5, probablemente porque la mayoría existía mucho antes de que detailsse especificaran y, por lo tanto, representan enfoques diferentes. Un elemento estándar permite que todos utilicen el mismo marcado para un tipo particular de contenido. Es por eso que tiene sentido crear un polyfill robusto .

Descargo de responsabilidad : este es un artículo bastante técnico y, aunque he intentado minimizar los fragmentos de código, el artículo todavía contiene bastantes de ellos. ¡Asi que preparate!

Las soluciones existentes están incompletas

No soy la primera persona que intenta implementar un polyfill de este tipo. Desafortunadamente, todas las demás soluciones presentan uno u otro problema:

  1. No hay soporte para contenido futuro El soporte para contenido futuro es extremadamente valioso para aplicaciones de una sola página. Sin él, tendría que invocar la función de inicialización cada vez que agregue contenido a la página. Básicamente, un desarrollador quiere poder acceder detailsal DOM y terminar con él, y no tener que jugar con JavaScript para ponerlo en marcha.
  2. toggleFalta el evento Este evento es una notificación de que un detailselemento ha cambiado de openestado. Idealmente, debería ser un evento DOM básico.

En este artículo usaremos Better-dom para simplificar las cosas. La razón principal es la función de extensiones en vivo , que resuelve el problema de invocar la función de inicialización para contenido dinámico. (Para obtener más información, lea mi artículo detallado sobre extensiones en vivo ). Además, Better-dom equipa las extensiones en vivo con un conjunto de herramientas que (todavía) no existen en el DOM básico pero que resultan útiles al implementar un polyfill como este.

Vea la demostración en vivo .

Echemos un vistazo más de cerca a todos los obstáculos que tenemos que superar para que esté detailsdisponible en navegadores que no lo admiten.

Soporte de contenido futuro

Para comenzar, necesitamos declarar una extensión activa para el “details”selector. ¿Qué pasa si el navegador ya admite el elemento de forma nativa? Luego necesitaremos agregar alguna detección de funciones. Esto es fácil con el segundo argumento opcional condition, que evita que la lógica se ejecute si su valor es igual a false:

// Invoke extension only if there is no native supportvar open = DOM.create("details").get("open");DOM.extend("details", typeof open !== "boolean", { constructor: function() { console.log("initialize details…"); }});

Como puede ver, estamos intentando detectar el soporte nativo comprobando la openpropiedad, que obviamente solo existe en navegadores que reconocen details.

Lo que lo DOM.extenddiferencia de una simple llamada document.querySelectorAlles que la constructorfunción también se ejecuta para contenido futuro. Y sí, funciona con cualquier biblioteca para manipular el DOM:

// You can use better-dom…DOM.find("body").append( "detailssummaryTITLE/summarypTEXT/p/details");// = logs "initialize details…"// or any other DOM library, like jQuery…$("body").append( "detailssummaryTITLE/summarypTEXT/p/details");// = logs "initialize details…"// or even vanilla DOM.document.body.insertAdjacentElement("beforeend", "detailssummaryTITLE/summarypTEXT/p/details");// = logs "initialize details…"

En las siguientes secciones, reemplazaremos la console.logllamada con una implementación real.

 

Implementación del summarycomportamiento

El detailselemento puede tomar summarycomo elemento hijo.

"El primer elemento de resumen secundario de los detalles, si hay uno presente, representa una descripción general de los detalles. Si no hay ningún elemento de resumen secundario, entonces el agente de usuario debe proporcionar su propia leyenda (por ejemplo, "Detalles")".

Agreguemos soporte para mouse. Un clic en el summaryelemento debería alternar el openatributo en el detailselemento principal. Así es como se ve usando Better-dom:

DOM.extend("details", typeof open !== "boolean", { constructor: function() { this .children("summary:first-child") .forEach(this.doInitSummary); }, doInitSummary: function(summary) { summary.on("click", this.doToggleOpen); }, doToggleOpen: function() { // We’ll cover the open property value later. this.set("open", !this.get("open")); }});

El childrenmétodo devuelve una matriz de elementos de JavaScript (no un objeto similar a una matriz como en el DOM básico). Por lo tanto, si no summaryse encuentra, entonces la doInitSummaryfunción no se ejecuta. Además, doInitSummaryy doToggleOpenson funciones privadas , siempre se invocan para el elemento actual. Entonces, podemos pasar this.doInitSummarya Array#forEachsin cierres adicionales y todo se ejecutará correctamente allí.

Tener soporte para teclado además de soporte para mouse también es bueno. Pero primero, hagamos summaryun elemento enfocable. Una solución típica es establecer el tabindexatributo en 0:

doInitSummary: function(summary) { // Makes summary focusable summary.set("tabindex", 0); …}

Ahora, el usuario que presione la barra espaciadora o la tecla "Entrar" debería cambiar el estado de details. En Better-dom, no hay acceso directo al objeto del evento. En su lugar, necesitamos declarar qué propiedades tomar usando un argumento de matriz adicional:

doInitSummary: function(summary) { … summary.on("keydown", ["which"], this.onKeyDown);}

Tenga en cuenta que podemos reutilizar la doToggleOpenfunción existente; para un keydownevento, simplemente realiza una verificación adicional en el primer argumento. Para el controlador de eventos de clic, su valor siempre es igual a undefinedy el resultado será este:

doInitSummary: function(summary) { summary .set("tabindex", 0) .on("click", this.doToggleOpen) .on("keydown", ["which"], this.doToggleOpen);},doToggleOpen: function(key) { if (!key || key === 13 || key === 32) { this.set("open", !this.get("open")); // Cancel form submission on the ENTER key. return false; }}

Ahora tenemos un detailselemento accesible con el mouse y el teclado.

 

summaryCasos de borde de elementos

El summaryelemento introduce varios casos extremos que debemos tener en cuenta:

1. ¿Cuándo summaryes un niño pero no el primer hijo?

Los proveedores de navegadores han intentado corregir este tipo de marcas no válidas moviéndose summaryvisualmente a la posición del primer hijo, incluso cuando el elemento no está en esa posición en el flujo del DOM. Estaba confundido por tal comportamiento, así que le pedí una aclaración al W3C . El W3C confirmó que summarydebe ser el primer hijo de details. Si verifica el marcado en la captura de pantalla anterior en Nu Markup Checker , fallará con el siguiente mensaje de error:

"Error: el resumen del elemento no se permite como hijo de los detalles del elemento en este contexto. […] Contextos en los que se puede utilizar el resumen del elemento: como primer hijo de un elemento de detalles".

Mi enfoque es mover el summaryelemento a la posición del primer hijo. En otras palabras, el polyfill corrige el marcado no válido por usted:

doInitSummary: function(summary) { // Make sure that summary is the first child if (this.child(0) !== summary) { this.prepend(summary); } …}

2. Cuando el summaryelemento no está presente

Como puede ver en la captura de pantalla anterior, los proveedores de navegadores insertan "Detalles" como leyenda summaryen este caso. El marcado permanece intacto. Desafortunadamente, no podemos lograr lo mismo sin acceder al DOM oculto , que lamentablemente tiene un soporte débil en la actualidad. Aún así, podemos configurar summarymanualmente para cumplir con los estándares:

constructor: function() { … var summaries = this.children("summary"); // If no child summary element is present, then the // user agent should provide its own legend (e.g. "Details"). this.doInitSummary( summaries[0] || DOM.create("summary`Details`"));}

Apoyo a openla propiedad

If you try the code below in browsers that support details natively and in others that don’t, you’ll get different results: AFrutados todo sobre frutas

details.open = true;// details changes state in Chrome and Safaridetails.open = false;// details state changes back in Chrome and Safari

In Chrome and Safari, changing the value of open triggers the addition or removal of the attribute. Other browsers do not respond to this because they do not support the open property on the details element.

Properties are different from simple values. They have a pair of getter and setter functions that are invoked every time you read or assign a new value to the field. And JavaScript has had an API to declare properties since version 1.5.

The good news is that one old browser we are going to use with our polyfill, Internet Explorer (IE) 8, has partial support for the Object.defineProperty function. The limitation is that the function works only on DOM elements. But that is exactly what we need, right?

There is a problem, though. If you try to set an attribute with the same name in the setter function in IE 8, then the browser will stack with infinite recursion and crashes. In old versions of IE, changing an attribute will trigger the change of an appropriate property and vice versa:

 

Object.defineProperty(element, "foo", { … set: function(value) { // The line below triggers infinite recursion in IE 8. this.setAttribute("foo", value); }});

So you can’t modify the property without changing an attribute there. This limitation has prevented developers from using the Object.defineProperty for quite a long time.

The good news is that I’ve found a solution.

Fix For Infinite Recursion In IE 8

Before describing the solution, I’d like to give some background on one feature of the HTML and CSS parser in browsers. In case you weren’t aware, these parsers are case-insensitive. For example, the rules below will produce the same result (i.e. a base red for the text on the page):

body { color: red; }/* The rule below will produce the same result. */BODY { color: red; }

The same goes for attributes:

el.setAttribute("foo", "1");el.setAttribute("FOO", "2");el.getAttribute("foo"); // = "2"el.getAttribute("FOO"); // = "2"

Moreover, you can’t have uppercased and lowercased attributes with the same name. But you can have both on a JavaScript object, because JavaScript is case-sensitive:

var obj = {foo: "1", FOO: "2"};obj.foo; // = "1"obj.FOO; // = "2"

Some time ago, I found that IE 8 supports the deprecated legacy argument lFlags for attribute methods, which allows you to change attributes in a case-sensitive manner:

  • lFlags [in, optional]
    • Type: Integer
    • Integer that specifies whether to use a case-sensitive search to locate the attribute.

Remember that the infinite recursion happens in IE 8 because the browser is trying to update the attribute with the same name and therefore triggers the setter function over and over again. What if we use the lFlags argument to get and set the uppercased attribute value:

// Defining the "foo" property but using the "FOO" attributeObject.defineProperty(element, "foo", { get: function() { return this.getAttribute("FOO", 1); }, set: function(value) { // No infinite recursion! this.setAttribute("FOO", value, 1); }});

As you might expect, IE 8 updates the uppercased field FOO on the JavaScript object, and the setter function does not trigger a recursion. Moreover, the uppercased attributes work with CSS too — as we stated in the beginning, that parser is case-insensitive.

Polyfill For The open Attribute

Ahora podemos definir una openpropiedad que funcione en todos los navegadores:

var attrName = document.addEventListener ? "open" : "OPEN";Object.defineProperty(details, "open", { get: function() { var attrValue = this.getAttribute(attrName, 1); attrValue = String(attrValue).toLowerCase(); // Handle boolean attribute value return attrValue === "" || attrValue === "open"; } set: function(value) { if (this.open !== value) { console.log("firing toggle event"); } if (value) { this.setAttribute(attrName, "", 1); } else { this.removeAttribute(attrName, 1); } }});

Comprueba cómo funciona:

 

details.open = true;// = logs "firing toggle event"details.hasAttribute("open"); // = truedetails.open = false;// = logs "firing toggle event"details.hasAttribute("open"); // = false

¡Excelente! Ahora hagamos llamadas similares, pero esta vez usando *Attributemétodos:

details.setAttribute("open", "");// = silence, but fires toggle event in Chrome and Safaridetails.removeAttribute("open");// = silence, but fires toggle event in Chrome and Safari

La razón de tal comportamiento es que la relación entre la openpropiedad y el atributo debe ser bidireccional . Cada vez que se modifica el atributo, la openpropiedad debe reflejar el cambio y viceversa.

La solución más simple para varios navegadores que he encontrado para este problema es anular los métodos de atributo en el elemento de destino e invocar a los configuradores manualmente. Esto evita errores y la penalización de rendimiento de legados propertychangey DOMAttrModifiedeventos. Los navegadores modernos admitenMutationObservers , pero eso no cubre el alcance de nuestro navegador.

Implementación final

Obviamente, seguir todos los pasos anteriores al definir un nuevo atributo para un elemento DOM no tendría sentido. Necesitamos una función de utilidad que oculte las peculiaridades y la complejidad de varios navegadores. Agregué una función de este tipo, llamada defineAttribute, en Better-dom.

El primer argumento es el nombre de la propiedad o atributo y el segundo es el objeto getand set. La función getter toma el valor del atributo como primer argumento. La función de establecimiento acepta el valor de la propiedad y la declaración devuelta se utiliza para actualizar el atributo. Esta sintaxis nos permite ocultar el truco para IE 8 donde se usa un nombre de atributo en mayúsculas detrás de escena:

constructor: function() { … this.defineAttribute("open", { get: this.doGetOpen, set: this.doSetOpen });},doGetOpen: function(attrValue) { attrValue = String(attrValue).toLowerCase(); return attrValue === "" || attrValue === "open";},doSetOpen: function(propValue) { if (this.get("open") !== propValue) { this.fire("toggle"); } // Adding or removing boolean attribute "open" return propValue ? "" : null;}

Tener un verdadero polirelleno para el openatributo simplifica nuestra manipulación del detailsestado del elemento. Nuevamente, esta API es independiente del marco :

// You can use better-dom…DOM.find("details").set("open", false);// or any other DOM library, like jQuery…$("details").prop("open", true);// or even vanilla DOM.document.querySelector("details").open = false;

Notas sobre el estilo

La parte CSS del polyfill es más simple. Tiene algunas reglas básicas de estilo:

summary:first-child ~ * { display: none;}details[open] * { display: block;}/* Hide native indicator and use pseudo-element instead */summary::-webkit-details-marker { display: none;}

No quería introducir ningún elemento adicional en el marcado, por lo que la opción obvia es darle estilo al ::beforepseudoelemento. Este pseudoelemento se utiliza para indicar el estado actual de details(según esté abierto o no). Pero IE 8 tiene algunas peculiaridades, como siempre, concretamente, la actualización del estado del pseudoelemento. Conseguí que funcionara correctamente solo cambiando el contentvalor de la propiedad:

 

details:before { content: '25BA'; …}details[open]:before { content: '25BC';}

Para otros navegadores, el truco del borde cero dibujará un triángulo CSS independiente de la fuente. Con una sintaxis de dos puntos para el ::beforepseudoelemento, podemos aplicar reglas a IE 9 y superiores:

details::before { content: ’; width: 0; height: 0; border: solid transparent; border-left-color: inherit; border-width: 0.25em 0.5em; … transform: rotate(0deg) scale(1.5);}details[open]::before { content: ’; transform: rotate(90deg) scale(1.5);}

La mejora final es una pequeña transición en el triángulo. Desafortunadamente, Safari no lo aplica por alguna razón (quizás un error), pero se degrada bien al ignorar la transición por completo:

details::before { … transition: transform 0.15s ease-out;}

Poniendolo todo junto

Hace un tiempo comencé a usar transpiladores en mis proyectos y son geniales. Los transpiladores mejoran los archivos fuente. Incluso puedes codificar en un lenguaje completamente diferente, como CoffeeScript en lugar de JavaScript o LESS en lugar de CSS, etc. Sin embargo, mi intención al usarlos es disminuir el ruido innecesario en el código fuente y aprender nuevas funciones en el futuro cercano. Es por eso que los transpiladores no van en contra de ningún estándar en mis proyectos: solo estoy usando algunas cosas adicionales de ECMAScript 6 (ES6) y postprocesadores CSS ( siendo Autoprefixer el principal).

Además, hablando de agrupación, rápidamente descubrí que distribuir *.cssarchivos junto con ellos *.jses un poco molesto. Mientras buscaba una solución, encontré HTML Imports , cuyo objetivo es resolver este tipo de problema en el futuro. En la actualidad, la función tiene un soporte de navegador relativamente débil . Y, francamente, agrupar todo eso en un único archivo HTML no es lo ideal.

Entonces, creé mi propio enfoque para la agrupación: Better-dom tiene una función DOM.importStylesque le permite importar reglas CSS en una página web. Esta función ha estado en la biblioteca desde el principio porque DOM.extendla usa internamente. Como de todos modos uso better-dom y transpilers en mi código, creé una tarea simple de trago:

gulp.task("compile", ["lint"], function() { var jsFilter = filter("*.js"); var cssFilter = filter("*.css"); return gulp.src(["src/*.js", "src/*.css"]) .pipe(cssFilter) .pipe(postcss([autoprefixer, csswring, …])) // need to escape some symbols .pipe(replace(/\|"/g, "\$")) // and convert CSS rules into JavaScript function calls .pipe(replace(/([^{]+){([^}]+)}/g, "DOM.importStyles("$1"" 




Tal vez te puede interesar:

  1. Más allá del navegador: de aplicaciones web a aplicaciones de escritorio
  2. Creación de un detector de habitaciones para dispositivos IoT en Mac OS
  3. Creación de una aplicación basada en Spotify utilizando Nuxt.js
  4. Cómo crear una API de Node.js para Ethereum Blockchain

Hacer un Polyfill completo para el elemento de detalles HTML5

Hacer un Polyfill completo para el elemento de detalles HTML5

Índice Las soluciones existentes están incompletas

programar

es

https://aprendeprogramando.es/static/images/programar-hacer-un-polyfill-completo-para-el-elemento-de-detalles-html5-868-0.jpg

2024-05-20

 

Hacer un Polyfill completo para el elemento de detalles HTML5
Hacer un Polyfill completo para el elemento de detalles HTML5

Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

 

 

Top 20