Pregunta ¿Cómo puedo convertir una matriz de nodos en una NodeList estática?


NOTA: Antes de que esta pregunta se considere duplicada, hay una sección al final de esta pregunta que explica por qué algunas preguntas similares no brindan la respuesta que estoy buscando.


Todos sabemos que es fácil convertir una NodeList en una matriz y hay muchas maneras de hacerlo:

[].slice.call(someNodeList)
// or
Array.from(someNodeList)
// etc...

Lo que estoy buscando es el reverso; ¿cómo puedo convertir una matriz de nodos en una NodeList estática?


¿Por qué quiero hacer esto?

Sin profundizar demasiado en las cosas, estoy creando un nuevo método para consultar elementos en la página, es decir:

Document.prototype.customQueryMethod = function (...args) {...}

Tratando de mantenerse fiel a la forma querySelectorAll funciona, quiero devolver un colección estática NodeList en lugar de una matriz.


He abordado el problema de tres maneras diferentes hasta ahora:

Intento 1:

Crear un fragmento de documento

function createNodeList(arrayOfNodes) {
    let fragment = document.createDocumentFragment();
    arrayOfNodes.forEach((node) => {
        fragment.appendChild(node);
    });
    return fragment.childNodes;
}

Si bien esto devuelve un NodeList, esto no funciona porque llamar appendChild elimina el nodo de su ubicación actual en el DOM (donde debería permanecer).

Otra variación de esto involucra cloning los nodos y devolver los clones. Sin embargo, ahora está devolviendo los nodos clonados, que no tienen ninguna referencia a los nodos reales en el DOM.


Intento 2:

Intentando "burlarse" del constructor NodeList

const FakeNodeList = (() => {

    let fragment = document.createDocumentFragment();
    fragment.appendChild(document.createComment('create a nodelist'));

    function NodeList(nodes) {
        let scope = this;
        nodes.forEach((node, i) => {
            scope[i] = node;
        });
    }

    NodeList.prototype = ((proto) => {
        function F() {
        }

        F.prototype = proto;
        return new F();
    })(fragment.childNodes);

    NodeList.prototype.item = function item(idx) {
        return this[idx] || null;
    };

    return NodeList;
})();

Y se usaría de la siguiente manera:

let nodeList = new FakeNodeList(nodes);

// The following tests/uses all work
nodeList instanceOf NodeList // true
nodeList[0] // would return an element
nodeList.item(0) // would return an element

Si bien este enfoque particular no elimina los elementos del DOM, causa otros errores, como al convertirlo en una matriz:

let arr = [].slice.call(nodeList);
// or
let arr = Array.from(nodeList);

Cada uno de los anteriores produce el siguiente error: Uncaught TypeError: Illegal invocation

También estoy tratando de evitar "imitar" una lista de nodos con un constructor de lista de nodos falso, ya que creo que probablemente tendrá futuras consecuencias imprevistas.


Intento 3:

Adjuntar un atributo temporal a los elementos para volver a consultarlos

function createNodeList(arrayOfNodes) {
    arrayOfNodes.forEach((node) => {
        node.setAttribute('QUERYME', '');
    });
    let nodeList = document.querySelectorAll('[QUERYME]');
    arrayOfNodes.forEach((node) => {
        node.removeAttribute('QUERYME');
    });
    return nodeList;
}

Esto estaba funcionando bien, hasta que descubrí que no funciona para ciertos elementos, como SVGes. No adjuntará el atributo (aunque solo probé esto en Chrome).


Parece que esto debería ser algo fácil de hacer, ¿por qué no puedo usar el constructor NodeList para crear una NodeList y por qué no puedo convertir una matriz en una NodeList de manera similar a las listas NodeLists en las matrices?

¿Cómo puedo convertir una matriz de nodos en una NodeList, de la manera correcta?


Preguntas similares que tienen respuestas que no me funcionan:

Las siguientes preguntas son similares a esta. Desafortunadamente, estas preguntas / respuestas no resuelven mi problema particular por las siguientes razones.

¿Cómo puedo convertir una matriz de elementos en una NodeList? La respuesta en esta pregunta usa un método que clona nodos. Esto no funcionará porque necesito tener acceso a los nodos originales.

Crear lista de nodos desde un solo nodo en JavaScript usa el enfoque de fragmento de documento (intento 1). Las otras respuestas intentan cosas similares en los intentos 2 y 3.

Creando un DOM NodeList esta usando E4X, y por lo tanto no se aplica. Y a pesar de que está usando eso, aún elimina los elementos del DOM.


32
2017-07-18 15:22


origen


Respuestas:


¿Por qué no puedo usar el constructor NodeList para crear una NodeList

Porque el Especificación DOM para el NodeList interfaz no especifica el atributo WebIDL [Constructor], por lo que no se puede crear directamente en las secuencias de comandos del usuario.

¿Por qué no puedo lanzar una matriz a una NodeList de manera similar a las listas de nodos que se lanzan a las matrices?

Sin duda, esta sería una función útil en su caso, pero no se especifica que exista dicha función en la especificación DOM. Por lo tanto, no es posible llenar directamente una NodeList de una serie de Nodes.

Aunque tengo serias dudas de que llamaría a esto "la manera correcta" de hacer las cosas, una solución fea es encontrar selectores de CSS que seleccionen únicamente los elementos deseados y pasar todos esos caminos hacia querySelectorAll como un selector de coma separado:

// find a CSS path that uniquely selects this element
function buildIndexCSSPath(elem) {
    var parent = elem.parentNode;

     // if this is the root node, include its tag name the start of the string
    if(parent == document) { return elem.tagName; } 

    // find this element's index as a child, and recursively ascend 
    return buildIndexCSSPath(parent) + " > :nth-child(" + (Array.prototype.indexOf.call(parent.children, elem)+1) + ")";
}

function toNodeList(list) {
    // map all elements to CSS paths
    var names = list.map(function(elem) { return buildIndexCSSPath(elem); });

    // join all paths by commas
    var superSelector = names.join(",");

    // query with comma-joined mega-selector
    return document.querySelectorAll(superSelector);
}

toNodeList([elem1, elem2, ...]);

Esto funciona mediante la búsqueda de cadenas de CSS para seleccionar de forma única cada elemento, donde cada selector tiene la forma html > :nth-child(x) > :nth-child(y) > :nth-child(z) .... Es decir, se puede entender que cada elemento existe como hijo de un hijo de un niño (etc.) en todo el elemento raíz. Al encontrar el índice de cada niño en la ruta del ancestro del nodo, podemos identificarlo de manera única.

Tenga en cuenta que esto no preservará Texttipo de nodos, porque querySelectorAll (y las rutas CSS en general) no pueden seleccionar nodos de texto.

Sin embargo, no tengo idea si esto será lo suficientemente bueno para sus propósitos.


18
2017-07-18 16:39



Aquí están mis dos centavos:

  • El documento es un objeto nativo y extenderlo puede no ser una buena idea.
  • NodeList es un objeto nativo con un constructor privado y sin métodos públicos para agregar elementos, y debe haber una razón para ello.
  • A menos que alguien pueda proporcionar un hack, no hay forma de crear y completar una NodeList sin modificar el documento actual.
  • NodeList es como una matriz, pero teniendo el item método que funciona igual que usar corchetes, con la excepción de regresar null en lugar de undefined cuando estás fuera del alcance. Puede devolver una matriz con el método de elemento implementado:

myArray.item= function (e) { return this[e] || null; }

PD: Tal vez estés tomando el enfoque equivocado y tu método de consulta personalizado podría simplemente envolver un document.querySelectorAll llamada que devuelve lo que estás buscando.


4
2017-07-18 16:19



Dado que parece que la creación de una NodeList real a partir de una matriz está teniendo graves inconvenientes, tal vez podría utilizar un objeto JS común con un prototipo hecho a sí mismo para emular una NodeList en su lugar. Al igual que:

var nodeListProto = Object.create({}, {
        item: {
            value: function(x) {
                return (Object.getOwnPropertyNames(this).indexOf(x.toString()) > -1) ? this[x] : null;
            },
            enumerable: true
        },
        length: {
            get: function() {
                return Object.getOwnPropertyNames(this).length;
            },
            enumerable: true
        }
    }),
    getNodeList = function(nodes) {
        var n, eN = nodes.length,
            list = Object.create(nodeListProto);
        for (n = 0; n < eN; n++) { // *
            Object.defineProperty(list, n.toString(), {
                value: nodes[n],
                enumerable: true
            });
        }
        return (list.length) ? list : null;
    };
// Usage:
var nodeListFromArray = getNodeList(arrayOfNodes);

Todavía hay algunos inconvenientes con esta solución. instanceof el operador no puede reconocer el objeto devuelto como NodeList. Además, los inicios de sesión de la consola y los directorios se muestran de forma diferente a los de NodeList.

(* = A for loop se usa para iterar la matriz pasada, de modo que la función también puede aceptar una NodeList pasada. Si prefieres un forEach loop, que también se puede usar, siempre que se pase solo una matriz).

Una demostración en vivo en jsFiddle.


4
2017-07-18 21:33



Puedes usar outerHTML propiedad de cada elemento, y agregarlo a un elemento padre (que creará document.createElement(), el tipo de elemento no importa). Por ejemplo, en ES6:

function getNodeList(elements) {
  const parentElement = document.createElement('div');
  // This can be a differnet element type, too (but only block (display: block;) element, because it impossible to put block element in inline element, and maybe 'elements' array contains a block element).
  let HTMLString = '';
  for (let element of elements) {
    HTMLString += element.outerHTML;
  }

  parentElement.innerHTML = HTMLString;

  return parentElement.childNodes;
}

1
2018-02-27 16:46