fix(js): make NodeList a real type and childNodes a NodeList

childNodes returned a plain Array (constructor "Array"), and NodeList
extended Array, so Array.isArray(list) was true and
Object.prototype.toString reported "[object Array]". A real browser
reports "[object NodeList]" and Array.isArray is false, which
feature-detection and fingerprinting scripts check.

NodeList is now its own type with the array-like surface scripts use
(indexed access, length, item, forEach, entries/keys/values, iteration),
and childNodes goes through it. Verified spread, for..of, and Array.from
still work.

Refs #335.
This commit is contained in:
SGavrl committed 2026-06-30 15:18:21 +00:00
1 parent a16e8d4fe3
commit 25ce541ec8
1 file changed
+25 -8
+25 -8
View File
@@ -444,9 +444,7 @@ class Node {
get parentElement() { const p = this.parentNode; return p && p.nodeType === 1 ? p : null; }
get childNodes() {
const ids = _domParse("child_nodes", this._nid) || [];
const list = ids.map(_wrap).filter(Boolean);
list.item = (i) => list[i] || null;
return list;
return _nodeList(ids.map(_wrap).filter(Boolean));
}
get firstChild() { return _wrap(+_dom("first_child", this._nid)); }
get lastChild() { return _wrap(+_dom("last_child", this._nid)); }
@@ -5149,17 +5147,36 @@ const _htmlCollectionProxy = {
function _isHTMLEl(el) {
return !!el && (el.namespaceURI === undefined || el.namespaceURI === "http://www.w3.org/1999/xhtml");
}
// Build a NodeList (no named access, per spec) for querySelectorAll. Kept light
// on purpose: querySelectorAll is the hottest query API.
// Build a NodeList (no named access, per spec) for querySelectorAll and
// childNodes. Kept light on purpose: querySelectorAll is the hottest query API.
function _nodeList(els) {
const nl = new NodeList();
for (let i = 0; i < els.length; i++) nl[nl.length] = els[i];
for (let i = 0; i < els.length; i++) nl[i] = els[i];
nl.length = els.length;
return nl;
}
globalThis.DOMTokenList = DOMTokenList;
globalThis.NodeList = class NodeList extends Array {
item(i) { return this[i] != null ? this[i] : null; }
// NodeList is its own type, not an Array subclass: in a real browser
// Array.isArray(nodeList) is false and Object.prototype.toString reports
// "[object NodeList]". Fingerprinting and feature-detection scripts check both.
// It keeps the array-like surface scripts actually use: indexed access, length,
// item(), forEach(), entries/keys/values, and iteration (so spread and for..of
// work).
globalThis.NodeList = class NodeList {
constructor() { this.length = 0; }
item(i) { i = i >>> 0; return this[i] != null ? this[i] : null; }
forEach(cb, thisArg) {
for (let i = 0; i < this.length; i++) cb.call(thisArg, this[i], i, this);
}
*[Symbol.iterator]() { for (let i = 0; i < this.length; i++) yield this[i]; }
*entries() { for (let i = 0; i < this.length; i++) yield [i, this[i]]; }
*keys() { for (let i = 0; i < this.length; i++) yield i; }
*values() { for (let i = 0; i < this.length; i++) yield this[i]; }
get [Symbol.toStringTag]() { return 'NodeList'; }
};
_markNative(NodeList);
_markNative(NodeList.prototype.item);
_markNative(NodeList.prototype.forEach);
// Live Range over the real DOM tree. dom/ranges/* tests are pure boundary-point
// algorithms (no layout, no editing engine), so a property-storing Range with
// correct tree-order comparison passes them. Mutating ops (extract/delete/