Presentamos extensiones en vivo para Better-DOM: qué son y cómo funcionan

 

 

 

  • SmashingConf Freiburg 2024

  • Índice
    1. Las responsabilidades de las extensiones en vivo
      1. Enlace de eventos simple
      2. Eventos en vivo y delegados
      3. Casos de uso de extensiones en vivo
    2. Breve mirada a la historia
      1. Componentes HTML
      2. Decoradores
    3. API de extensiones en vivo
      1. DOM.extender
      2. La propiedad del constructor
      3. Extendiendo * Elementos
      4. Múltiples extensiones en vivo en el mismo elemento
      5. Herencia
      6. Escribir pruebas con DOM.mock
      7. Detección de funciones (en better-dom 1.7)
    4. Conclusión
      1. Otras lecturas

    Después de escribir su último artículo, " Escribir una mejor biblioteca de JavaScript para el DOM ", Maksim Chemerisuk se dio cuenta de que es importante comprender qué son exactamente las extensiones activas y cómo funcionan, ya que el tema es extremadamente complejo. En el artículo de hoy, responderá la mayoría de las preguntas que se formularon sobre las " extensiones en vivo ".

     

    Después de escribir recientemente un artículo sobre " Escribir una mejor biblioteca de JavaScript para el DOM ", me di cuenta de que el tema es realmente muy complejo y que es importante comprender qué son exactamente las extensiones activas y cómo funcionan. En el artículo de hoy, responderé la mayoría de las preguntas que se hicieron sobre las " extensiones en vivo " y lo ayudaré a comenzar con este nuevo concepto.

    Las responsabilidades de las extensiones en vivo

    El manejo de eventos es uno de los principios clave al trabajar con el DOM. Los eventos son el medio principal para recibir comentarios de la interacción del usuario.

    Enlace de eventos simple

    En este primer ejemplo, la documentación y los tutoriales que cubren eventos DOM es lo que yo llamo "vinculación de eventos simple". Adjunta un detector para el evento deseado en el elemento DOM en el que espera que suceda.

    link.addEventListener("click", function(e) { // do something when the link is clicked}, false);

    El primer argumento indica el tipo de evento, el segundo argumento es un oyente y el tercer argumento define una fase del evento (la llamada "burbuja" o "captura"). La razón por la que existe el último argumento es porque la mayoría de los eventos DOM atraviesan el árbol DOM desde el nodo del documento hasta el nodo de destino (fase de captura) y de regreso al nodo del documento (fase de burbuja). Este proceso se denomina "flujo de eventos" y ofrece varias funciones potentes.

    Eventos en vivo y delegados

    En lugar de adjuntar un controlador para cada elemento de un grupo, podemos adjuntar un oyente a un ancestro compartido por todos los elementos de ese grupo específico. Luego, podemos determinar dónde tuvo lugar un evento utilizando la targetpropiedad del objeto de evento, pasado al oyente. Esto se conoce como “delegación de eventos”:

    list.addEventListener("click", function(e) { if (e.target.tagName === "LI") { // do something when a child li element is clicked }}, false);

    Al tener todos los controladores de eventos en un padre en particular, podemos actualizar la innerHTMLpropiedad de este elemento sin perder la capacidad de escuchar eventos en busca de nuevos elementos. La función se llamó "Eventos en vivo" en jQuery y rápidamente se hizo popular debido a su capacidad de filtrar eventos mediante un selector de CSS. Posteriormente, los eventos delegados los reemplazaron debido a su flexibilidad al permitir vincular un oyente a cualquier elemento dentro del árbol del documento.

    Pero ni siquiera la delegación de eventos soluciona los siguientes problemas:

    • Cuando se requiere una mutación DOM después de que un nuevo elemento (que coincide con un selector específico) ingresa al árbol del documento,
    • Cuando un elemento debe inicializarse en un evento excesivo como scrollo mousemove,
    • O en eventos que no son burbujeantes, por ejemplo load, , error, etc.

    Esto es lo que pretenden resolver las Live Extensions.

    Casos de uso de extensiones en vivo

    Eche un vistazo al siguiente diagrama que explica las responsabilidades:

     

    1. Mutaciones DOM para elementos existentes y futuros

    Imagine que desea desarrollar un widget de selección de fechas reutilizable. En HTML5, hay un input type="date"elemento basado en estándares que podría usarse para crear un polyfill. Pero el problema es que este elemento se ve y se comporta de manera muy diferente de un navegador a otro:

    Elemento de entrada de fecha en diferentes navegadores.

    La única forma de hacer que el elemento se comporte de manera consistente es establecer el valor del atributo de tipo en “text”. Esto cancelará una implementación heredada y permitirá que JavaScript cree la suya propia. Intente definir una extensión en vivo con el siguiente ejemplo:

    DOM.extend("input[type=date]", { constructor: function() { // cancel browser-specific implementation this.set("type", "text"); // make your own styleable datepicker, // attach additional event handlers etc. }});

    2. Devoluciones de llamadas de consultas de medios

    Recomiendo encarecidamente leer el artículo de Paul Hayes sobre cómo " Usar transiciones CSS para vincular consultas de medios y JavaScript ".

    "Un problema común en el diseño responsivo es la vinculación de las consultas de medios de CSS3 y JavaScript. Por ejemplo, en una pantalla más grande podemos cambiar el estilo, pero podría ser útil usar JavaScript e incorporar contenido diferente al mismo tiempo, por ejemplo, imágenes de mayor calidad. "

    Paul fue probablemente el primero que empezó a utilizar la "fuerza oculta" de los eventos de animación CSS3 para resolver problemas relacionados con las mutaciones. Las extensiones en vivo funcionan con el mismo truco, por lo tanto, puedes usarlas para realizar modificaciones DOM dependiendo de la ventana gráfica actual:

    DOM.extend(".rwd-menu", { constructor: function() { var viewportWidth = DOM.find("html").get("clientWidth"); if (viewportWidth 768) { // hide ul and construct Emmet abbreviation for a // select element that should be used on small screens this.hide().after("select[onchange='location=this.value']" + this.children("li").reduce(function(memo, item) { var text = item.get("textContent"), href = item.find("a").get("href"); memo.push("option[value=" + href + "]{" + text + "}"); return memo; }, []).join("^")); } }});

    3. Consultas de medios de elementos

    En 2011, Andy Hume implementó un script para aplicar estilos según las dimensiones de un elemento en particular (no una ventana gráfica, como para las consultas de medios). Posteriormente, esta técnica se denominó “consultas de medios de elementos”:

    "Las consultas de medios funcionan muy bien cuando se desea ajustar los diseños principales del sitio, pero son menos adecuadas para cambiar estilos en un nivel más pequeño y granular".

    Con la ayuda de extensiones en vivo, es fácil implementar la compatibilidad con consultas de medios de elementos utilizando el offsetmétodo:

    DOM.extend(".signup-form", { constructor: function() { var currentWidth = this.offset().width; // add extra class depending on current width if (currentWidth 150) { this.addClass("small-signup-form"); } else if (currentWidth 300) { this.addClass("wide-signup-form"); } }});

    4. Conecte de manera eficiente un oyente global a eventos frecuentes

     

    DOM.extend(".detectable", { constructor: function() { // mousemove bubbles but it’s usually a very bad // idea to listen to such event on a document level // but live extensions help to solve the issue this.on("mousemove", this.onMouseMove, ["pageX", "pageY"]); }, onMouseMove: function(x, y) { // just output current coordinates into console console.log("mouse position: x=" + x + ", y=" + y); }});

    5. Listado de eventos no burbujeantes a nivel de documento

    DOM.extend("img.safe-img", { constructor: function() { // error event doesn’t bubble so it’s not // possible to do the same using live events this.on("error", this.onError); }, onError: function() { // show a predefined png if an image download fails this.src = "/img/download-failed.png" }});

    Breve mirada a la historia

    Los problemas que las extensiones en vivo pretenden resolver no son del todo nuevos, por supuesto. Existen diferentes enfoques que abordan las cuestiones antes mencionadas. Echemos un vistazo rápido a algunos de ellos.

    Componentes HTML

    Internet Explorer comenzó a admitir comportamientos DHTML con IE 5.5:

    "Los comportamientos DHTML son componentes que encapsulan una funcionalidad o comportamiento específico en una página. Cuando se aplica a un elemento HTML estándar en una página, un comportamiento mejora el comportamiento predeterminado de ese elemento".

    Para adjuntar comportamiento a elementos futuros, Internet Explorer utilizó un *.htcarchivo con una sintaxis especial. Aquí hay un ejemplo que ilustra cómo solíamos trabajar :hoveren elementos en lugar de a:

    PUBLIC:COMPONENT URN="urn:msdn-microsoft-com:workshop" PUBLIC:ATTACH EVENT="onmouseover" ONEVENT="Hilite()" / PUBLIC:ATTACH EVENT="onmouseout" ONEVENT="Restore()" / SCRIPT LANGUAGE="JScript" var normalColor, normalSpacing; function Hilite() { normalColor = currentStyle.color; normalSpacing= currentStyle.letterSpacing; runtimeStyle.color = "red"; runtimeStyle.letterSpacing = 2; } function Restore() { runtimeStyle.color = normalColor; runtimeStyle.letterSpacing = normalSpacing; }/SCRIPT/PUBLIC:COMPONENT

    Si proporcionó el código mencionado anteriormente en el hilite.htcarchivo, podrá acceder a él dentro de CSS a través de la behaviorpropiedad:

    li { behavior: url(hilite.htc);}

    Me sorprendió mucho descubrir que los componentes HTML admiten la creación de etiquetas personalizadas (a partir de la versión 5.5), tienen limitaciones de dominio único y toneladas de otras cosas que probablemente nunca antes hayas usado. A pesar de que Microsoft envió una propuesta al W3C , otros proveedores de navegadores decidieron no admitir esta función. Como resultado, se eliminaron los componentes HTML de Internet Explorer 10.

    Decoradores

    En mi artículo anterior , mencioné los decoradores que forman parte de los componentes web. Así es como puedes implementar el indicador de estado abierto/cerrado del detailselemento usando decoradores:

     

    decorator script function clicked(event) { event.target.setAttribute('open', 'open'); } [{selector: '#summary', type: 'click', handler: clicked}]; /script template a blacktriangleright; content select="summary"/content /a /template/decoratordecorator script function clicked(event) { event.target.removeAttribute('open'); } [{selector: '#summary', type: 'click', handler: clicked}]; /script template a blacktriangledown; content select="summary"/content /a content/content /template/decorator

    Los decoradores también se aplican usando la decoratorpropiedad especial en CSS:

    details { decorator: url(#details-closed);}details[open] { decorator: url(#details-open);}

    Rápidamente notarás que esto se acerca mucho a lo que Microsoft propuso en Componentes HTML . La diferencia es que en lugar de archivos HTC separados, los decoradores son elementos HTML que se pueden definir dentro del mismo documento. El ejemplo anterior sólo se proporciona para mostrar que la plataforma web está trabajando en estos temas, ya que los decoradores aún no están especificados correctamente.

    API de extensiones en vivo

    Mientras diseñaba API para extensiones en vivo, decidí seguir las siguientes reglas:

    1. Las extensiones en vivo deben declararse en JavaScript. Creo firmemente que todo lo que de alguna manera cambia el comportamiento de un elemento debe presentarse en un archivo JavaScript. (Tenga en cuenta que Better-dom inserta una nueva regla CSS detrás de escena, pero esto incluye solo detalles de implementación).
    2. Las API deberían ser fáciles de usar. Sin formatos de archivo complicados ni nuevos elementos HTML: solo se requiere una pequeña cantidad de conocimiento relacionado con el constructor y los controladores de eventos para comenzar a desarrollar una extensión en vivo (por lo tanto, la barrera de entrada debe ser baja).

    Como resultado, sólo hay dos métodos con los que lidiar: DOM.extendy DOM.mock. Que dia se celebra hoy

    DOM.extender

    DOM.extenddeclara una extensión en vivo. Acepta un selector CSS como primer argumento que define qué elementos desea capturar. Consejo general: intenta que el selector sea sencillo.

    Lo ideal es utilizar sólo un nombre de etiqueta, clase o atributo con o sin valor o sus combinaciones entre sí. Estos selectores se pueden probar más rápido sin necesidad de recurrir a un matchesSelectormétodo costoso .

    El segundo argumento es una definición de extensión en vivo. Todas las propiedades del objeto se combinarán con una interfaz contenedora de elementos, excepto el constructor y los controladores de eventos .

    Veamos un ejemplo sencillo. Supongamos que tenemos dicho elemento en una página web:

    div.../div

    La tarea es mostrarlo como un diálogo modal. Así es como podría verse la extensión en vivo:

    DOM.extend(".modal-dlg", { constructor: function() { var backdrop = DOM.create("div.modal-dlg-backdrop"); // using bind to store reference to backdrop internally this.showModal = this.showModal.bind(this, backdrop); // we will define event handlers later }, showModal: function(backdrop) { this.show(); backdrop.show(); }});

    Ahora puedes acceder al método público showModalen cualquier elemento (presente o futuro) que tenga la modal-dlgclase (en nuestro caso este es el signin-formdiv):

     

    var signinForm = DOM.find(".signin-form");DOM.find(".signin-btn").on("click", function() { // the signin button doesn’t have the modal-dlg class // so it’s interface doesn’t contain the showModal method console.log(this.showModal); // = undefined signinForm.showModal(); // = shows the signin dialog});

    Nota : El better-dom-legacy.jsarchivo que se incluye condicionalmente para las versiones 8 y 9 de Internet Explorer contiene la biblioteca es5-shim para que pueda utilizar de forma segura las funciones de EcmaScript 5 basadas en estándares (como Function.prototype.bind) en su código. He estado usando bindmucho el método en mi código para crear métodos comprobables fácilmente.

    La propiedad del constructor

    La función constructora se llama cuando un elemento se vuelve visible . Esto se debe al animationstartevento que se utiliza para implementar DOM.extend. Los navegadores son inteligentes y no activan eventos de animación para elementos ocultos. Esta inicialización diferida a veces ahorra recursos, pero tenga cuidado al acceder a elementos inicialmente ocultos.

    En versiones anteriores de Internet Explorer, como 8 y 9, contentreadyel evento de better-dom-legacy.htcse utiliza para implementar extensiones en vivo. Por lo tanto, la función constructora se ejecuta inmediatamente en estos navegadores, incluso para elementos ocultos.

    Nota : Tenga en cuenta que no debe depender del tiempo cada vez que se inicializa una extensión. ¡La inicialización real de una extensión activa varía según el navegador!

    El constructor suele ser el lugar donde adjuntas controladores de eventos y realizas mutaciones DOM cuando sea necesario. Una vez que se haya completado la función, todos los métodos que comiencen con "on" (en Better-dom 1.7 también "do") seguido de una letra mayúscula, los controladores de eventos, se eliminarán de la interfaz del contenedor de elementos.

    Actualicemos nuestra .signin-formextensión en vivo con la ayuda de un botón de cerrar y la ESCclave:

    DOM.extend(".modal-dlg", { constructor: function() { var backdrop = DOM.create("div.modal-dlg-backdrop"), closeBtn = this.find(".close-btn"); this.showModal = this.showModal.bind(this, backdrop); // handle click on the close button and ESC key closeBtn.on("click", this.onClose.bind(this, backdrop)); DOM.on("keydown", this.onKeyDown.bind(this, closeBtn), ["which"]) }, showModal: function(backdrop) { this.show(); backdrop.show(); }, onClose: function(backdrop) { this.hide(); frame.hide(); }, onKeyDown: function(closeBtn, which) { if (which === 27) { // close dialog by triggering click event closeBtn.fire("click"); } }});

    A pesar de que la extensión en vivo contiene ambos métodos onClosey onKeyDown, no se mezclarán en la interfaz contenedora de elementos:

     

    var signinForm = DOM.find(".signin-form");console.log(signinForm.onClose); // = undefinedconsole.log(signinForm.onKeyDown); // = undefined

    Este tipo de comportamiento existe simplemente porque puede tener múltiples extensiones activas para un solo elemento que pueden sobrecargar los métodos públicos entre sí y producir resultados inesperados. Para los controladores de eventos, esto no es posible; existen sólo dentro de la función constructora.

    Extendiendo * Elementos

    A veces es útil ampliar todos los contenedores de elementos con un método (o métodos) en particular. Pero, de nuevo, también puedes usar el selector universal para resolver el problema:

    DOM.extend("*", { gesture: function(type, handler) { // implement gestures support }});…DOM.find("body").gesture("swipe", function() { // handle a swipe gesture on body});

    El *selector tiene un comportamiento especial: todas las propiedades de declaración de extensión se inyectarán directamente en el prototipo del contenedor del elemento, excepto el constructor, que se ignora por completo. Por lo tanto, no existe ninguna penalización de rendimiento que normalmente se asocia con el selector universal.

    Nota : Nunca pase selectores más específicos como .some-class *into DOM.extendporque son lentos y no tienen el mismo comportamiento mencionado anteriormente.

    Múltiples extensiones en vivo en el mismo elemento

    La mayoría de las veces, tiene sentido dividir una gran extensión en vivo en varias partes para reducir la complejidad. Por ejemplo, es posible que tenga un elemento de este tipo en su página:

    div/div

    Hay dos extensiones diferentes adjuntas. La .infinite-scrollextensión implementa un conocido patrón de desplazamiento infinito, por ejemplo, es responsable de cargar contenido nuevo. Al mismo tiempo, la .chatextensión muestra información sobre herramientas cada vez que un usuario pasa el cursor sobre una imagen de usuario, agrega emoticones en los mensajes, etc. Sin embargo, sea preciso con múltiples extensiones: aunque todos los controladores de eventos se hayan eliminado de la interfaz, es posible que aún tenga métodos públicos que se cruzan entre sí.

    Herencia

    Las extensiones en vivo respetan el orden de declaración; puede utilizar esto a su favor y desarrollar su propia jerarquía de componentes. El enlace tardío ayuda a declarar controladores de eventos reemplazables y la sobrecarga de métodos permite redefinir la implementación de un método en una extensión secundaria:

    DOM.extend(".my-widget", { constructor: function() { this.on("click", "_handleClick"); }, showMessage: function() { }});DOM.extend(".my-button", { _handleClick: function() { console.log("I am a button!"); }, showMessage: function() { alert("I am a button message!"); }});

    Si observa más de cerca el código anterior, notará que la .my-buttonextensión no adjunta un detector de clics. El registro se realiza con la ayuda de enlace tardío en lugar de un simple controlador de eventos en .my-widget. El enlace tardío es una elección perfecta en este caso: incluso si un niño no implementa, _handleClickno habrá ningún error ya que el controlador será ignorado silenciosamente.

     

    Si bien es posible distribuir la funcionalidad entre varios módulos, no se recomienda en el uso diario. Verifica si realmente necesitas ir en esta dirección, porque es la más compleja.

    Escribir pruebas con DOM.mock

    Un requisito para un widget de alta calidad es la cobertura de prueba. Los nuevos elementos son capturados por una extensión en vivo de forma asincrónica, por lo que no es tan fácil simplemente crearlos en la memoria. Para solucionar este problema, better-dom tiene la DOM.mockfunción:

    var myButton = DOM.mock("button.my-button");

    DOM.mockcrea elementos, al igual que DOM.create. Además, aplica sincrónicamente las extensiones en vivo registradas a los elementos recién creados. Para mayor comodidad, todos los objetos contenedores creados por DOM.mockconservan los controladores de eventos (p. ej. onClick), para que pueda probarlos.

    De vez en cuando, es posible que necesites crear una instancia "falsa" de un elemento. Úselo DOM.mocksin argumentos para crear tal objeto:

    console.log(DOM.mock().length); // = 0

    Una prueba para la extensión en vivo del diálogo modal presentada anteriormente podría verse así (yo uso Jasmine ):

    describe(".modal-dlg", function() { var dlg, backdrop; beforeEach(function() { dlg = DOM.mock("div.modal-dlg"); backdrop = DOM.mock(); }); it("should hide itself and backdrop on close", function() { var dlgSpy = spyOn(dlg, "hide"), backdropSpy = spyOn(backdrop, "hide"); dlg.onClose(backdrop); expect(dlgSpy).toHaveBeenCalled(); expect(backdropSpy).toHaveBeenCalled(); }); it("should show itself and backdrop on show", function() { var dlgSpy = spyOn(dlg, "show"), backdropSpy = spyOn(backdrop, "show"); dlg.showModal(backdrop); expect(dlgSpy).toHaveBeenCalled(); expect(backdropSpy).toHaveBeenCalled(); });});

    Detección de funciones (en better-dom 1.7)

    Hay algunos casos en los que filtrar con un selector CSS no es lo suficientemente flexible. Por ejemplo, digamos que desea declarar una extensión activa pero solo para navegadores que admiten (o no) una característica particular. Es posible que deba ejecutar pruebas en un navegador sin cabeza como PhantomJS que admita la función de forma nativa. A partir de Better-dom 1.7, DOM.extendse admite el argumento opcional condition.

    Supongamos que necesitamos crear un polyfill para el placeholderatributo . No tiene sentido implementarlo para navegadores que tienen soporte integrado. A continuación se muestra un ejemplo de cómo podría verse la detección de características:

    var supportsPlaceholder = typeof DOM.create("input") .get("placeholder") === "string";

    Al usar solo una simple declaración "Si" como se muestra en el siguiente ejemplo, no podremos probar el widget porque PhantomJS admite el placeholderatributo y la extensión activa nunca se declarará.

    if (!supportsPlaceholder) { DOM.extend("[placeholder]", { // implement placeholder support };}

    Para resolver este problema, puede utilizar un conditionargumento adicional DOM.extendque podría ser booleano o una función:

     

    DOM.extend("[placeholder]", !supportsPlaceholder, { constructor: function() { … }, onFocus: function() { … }, onBlur: function() { … }});

    DOM.mockignora el conditionargumento, por lo que puede acceder a todos los métodos de la [placeholder]extensión incluso si el navegador actual pasa la verificación:

    var input = DOM.mock("input[placeholder=test]");typeof input.onFocus; // = "function"

    Conclusión

    Las extensiones en vivo (y Better-Dom como implementación del concepto) son una buena base sobre la cual construir cuando su objetivo no está seguro, por ejemplo, al crear un polyfill que puede usarse o no en un sitio en particular. O widgets regulares que pueden ser necesarios o no, dependiendo de alguna llamada AJAX.

    Las extensiones en vivo tienen como objetivo separar la declaración y el uso de widgets. Proporcionan un acoplamiento flexible (o más bien un desacoplamiento) de cualquier componente basado en DOM y permiten que su código se vuelva más pequeño, más limpio y más fácil de mantener. Incluso puedes combinar estas piezas independientes con cualquier marco existente en el mercado (o con el DOM básico, por supuesto).

    Quizás ahora estés pensando: "Pero espera, hay proyectos como Polymer o x-tags , ¿verdad?" Bueno, las extensiones en vivo cubren un área diferente ; No se trata de etiquetas personalizadas, sino de ampliar las existentes. Prefiero una forma basada en estándares (si es posible) de crear widgets de interfaz de usuario, por lo que hacer polyfills es mi elección.

    Better-dom también tiene otra ventaja: una extensión en vivo cuidadosamente diseñada no te obliga a reescribir el marcado de un sitio web usando etiquetas diferentes. Todo lo que necesita es simplemente incluir un archivo de script en su página. Los elementos basados ​​en estándares pueden funcionar potencialmente sin JavaScript, por lo que se degradan bien cuando está deshabilitado. Y la compatibilidad con el navegador de la biblioteca le permite comenzar a utilizar extensiones en vivo de inmediato.

    No dude en compartir sus opiniones en la sección de comentarios a continuación o en la página de inicio del proyecto Better-Dom .

    Otras lecturas

    • Eventos de entrada del navegador: ¿Podemos hacerlo mejor que el clic?
    • Análisis de las características de la red utilizando JavaScript y DOM
    • Creación de una sencilla lista de tareas pendientes sin conexión para varios navegadores
    • Eventos de JavaScript y respuesta al usuario

    (il, señor)Explora más en

    • Codificación
    • javascript
    • Técnicas
    • jQuery





    Tal vez te puede interesar:

    1. ¿Deberían abrirse los enlaces en ventanas nuevas?
    2. 24 excelentes tutoriales de AJAX
    3. 70 técnicas nuevas y útiles de AJAX y JavaScript
    4. Más de 45 excelentes recursos y repositorios de fragmentos de código

    Presentamos extensiones en vivo para Better-DOM: qué son y cómo funcionan

    Presentamos extensiones en vivo para Better-DOM: qué son y cómo funcionan

    SmashingConf Freiburg 2024 Índice Las responsabilidades de las extensiones en vivo

    programar

    es

    https://aprendeprogramando.es/static/images/programar-presentamos-extensiones-en-vivo-para-better-dom-que-son-y-como-funcionan-837-0.jpg

    2024-05-20

     

    Presentamos extensiones en vivo para Better-DOM: qué son y cómo funcionan
    Presentamos extensiones en vivo para Better-DOM: qué son y cómo funcionan

    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