Skip to content

Commit dc2dcf5

Browse files
committed
Fix handling of Array-like DOM objects
1 parent 5d6dde2 commit dc2dcf5

3 files changed

Lines changed: 109 additions & 24 deletions

File tree

lib/match/selector.js

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,61 @@ function isHTMLDocument(document) {
159159
return document.contentType === 'text/html'
160160
}
161161

162-
// Element utilities
162+
/**
163+
* @param {DOMTokenList|HTMLCollection|HTMLSelectElement|NamedNodeMap|NodeList} list
164+
* @param {function} predicate
165+
* @returns {boolean}
166+
*/
167+
function every(list, predicate) {
168+
for (let index = 0; index < list.length; index++) {
169+
if (!predicate(list.item(index))) {
170+
return false
171+
}
172+
}
173+
return true
174+
}
175+
176+
/**
177+
* @param {DOMTokenList|HTMLCollection|HTMLSelectElement|NamedNodeMap|NodeList} list
178+
* @param {function} predicate
179+
* @returns {*[]}
180+
*/
181+
function filter(list, predicate) {
182+
const filtered = []
183+
for (let index = 0; index < list.length; index++) {
184+
const item = list.item(index)
185+
if (predicate(item)) {
186+
filtered.push(item)
187+
}
188+
}
189+
return filtered
190+
}
191+
192+
/**
193+
* @param {DOMTokenList|HTMLCollection|HTMLSelectElement|NamedNodeMap|NodeList} list
194+
* @param {function} predicate
195+
* @returns {*}
196+
*/
197+
function find(list, predicate) {
198+
for (let index = 0; index < list.length; index++) {
199+
const item = list.item(index)
200+
if (predicate(item)) {
201+
return item
202+
}
203+
}
204+
}
205+
206+
/**
207+
* @param {DOMTokenList|HTMLCollection|HTMLSelectElement|NamedNodeMap|NodeList} list
208+
* @param {function} predicate
209+
* @returns {boolean}
210+
*/
211+
function some(list, predicate) {
212+
return !!find(list, predicate)
213+
}
214+
215+
216+
// Pseudo-element matching utilities
163217

164218
/**
165219
* @param {ElementImpl} meter
@@ -408,7 +462,7 @@ function isDefault(element) {
408462
if (isOfType(element, 'option')) {
409463
return element.hasAttribute('selected')
410464
}
411-
return element === element.form?.elements.find(isSubmitButton)
465+
return element.form && element === find(element.form.elements, isSubmitButton)
412466
}
413467

414468
/**
@@ -592,7 +646,7 @@ function isIndeterminate(element) {
592646
return indeterminate
593647
}
594648
if (type === 'radio' && name) {
595-
const group = form.elements.filter(control => control.type === 'radio' && name === control.name)
649+
const group = filter(form.elements, control => control.type === 'radio' && name === control.name)
596650
return 1 < group.length && group.every(control => !control.checked)
597651
}
598652
}
@@ -607,7 +661,7 @@ function isIndeterminate(element) {
607661
*/
608662
function isInvalid(element) {
609663
if (isOfType(element, 'form', 'fieldset')) {
610-
return element.elements.some(element => isCandidateForConstraintValidation(element) && isInvalid(element))
664+
return some(element.elements, element => isCandidateForConstraintValidation(element) && isInvalid(element))
611665
}
612666
return isCandidateForConstraintValidation(element) && !element.validity.valid
613667
}
@@ -948,11 +1002,14 @@ function isUnchecked(element) {
9481002
*/
9491003
function isValid(element) {
9501004
if (isOfType(element, 'form', 'fieldset')) {
951-
return element.elements.every(element => !isCandidateForConstraintValidation(element) || isValid(element))
1005+
return every(element.elements, element => !isCandidateForConstraintValidation(element) || isValid(element))
9521006
}
9531007
return isCandidateForConstraintValidation(element) && element.validity.valid
9541008
}
9551009

1010+
1011+
// Tree traversal utilities
1012+
9561013
/**
9571014
* @param {ElementImpl} element
9581015
* @param {boolean} first
@@ -1318,7 +1375,8 @@ function matchAttributeNameSelector(element, selector, { namespaces = new Map })
13181375
name = toLowerCase(name)
13191376
}
13201377
if (prefix === '*') {
1321-
for (const attribute of element.attributes) {
1378+
for (let index = 0; index < element.attributes.length; index++) {
1379+
const attribute = element.attributes.item(index)
13221380
if (attribute.localName === name) {
13231381
return attribute
13241382
}

test/dom.js

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,10 @@ export class DOMTokenList {
7575
export class HTMLCollection {
7676

7777
/**
78-
* @param {NodeList} list
78+
* @param {NodeList} [list]
7979
*/
80-
constructor(list) {
81-
this._list = list._list
80+
constructor(list = []) {
81+
this._list = list
8282
}
8383

8484
/**
@@ -97,6 +97,31 @@ export class HTMLCollection {
9797
}
9898
}
9999

100+
export class NamedNodeMap {
101+
102+
/**
103+
* @param {NodeList} [list]
104+
*/
105+
constructor(list = []) {
106+
this._list = list
107+
}
108+
109+
/**
110+
* @param {number} index
111+
* @returns {Element|null}
112+
*/
113+
item(index) {
114+
return this._list[index]
115+
}
116+
117+
/**
118+
* @returns {number}
119+
*/
120+
get length() {
121+
return this._list.length
122+
}
123+
}
124+
100125
export class NodeList {
101126

102127
/**
@@ -248,7 +273,7 @@ export class Document extends Node {
248273
globalThis.document = this
249274

250275
this.activeViewTransition = activeViewTransition
251-
this.children = new HTMLCollection(this.childNodes)
276+
this.children = new HTMLCollection(this.childNodes._list)
252277
this.location = new URL(url)
253278
this.URL = url
254279

@@ -290,7 +315,7 @@ export class DocumentFragment extends Node {
290315
*/
291316
constructor(properties) {
292317
super(properties)
293-
this.children = new HTMLCollection(this.childNodes)
318+
this.children = new HTMLCollection(this.childNodes._list)
294319
}
295320

296321
/**
@@ -350,17 +375,17 @@ export class Element extends Node {
350375
selectors = [],
351376
} = properties
352377

353-
this.attributes = attributes.map(({ localName, namespaceURI = null, value = '' }) =>
354-
({ localName, namespaceURI, ownerElement: this, value }))
355-
this.children = new HTMLCollection(this.childNodes)
378+
this.attributes = new NamedNodeMap(attributes.map(({ localName, namespaceURI = null, value = '' }) =>
379+
({ localName, namespaceURI, ownerElement: this, value })))
380+
this.children = new HTMLCollection(this.childNodes._list)
356381
this.classList = new DOMTokenList(this.getAttribute('class')?.split(' '))
357382
this.indeterminate = indeterminate
358383
this.isContentEditable = isContentEditable
359384
this.localName = localName
360385

361386
this.form = form
362-
form?.elements.push(this)
363-
fieldSet?.elements.push(this)
387+
form?.elements._list.push(this)
388+
fieldSet?.elements._list.push(this)
364389
this.name = this.getAttribute('name') ?? ''
365390
this.required = !!this.getAttributeNode('required')
366391
this.slot = this.getAttribute('slot') ?? ''
@@ -418,7 +443,7 @@ export class Element extends Node {
418443
* @returns {object|null}
419444
*/
420445
getAttributeNodeNS(namespace, name) {
421-
return this.attributes.find(attribute => attribute.localName === name && attribute.namespaceURI === namespace) ?? null
446+
return this.attributes._list.find(attribute => attribute.localName === name && attribute.namespaceURI === namespace) ?? null
422447
}
423448

424449
/**
@@ -460,7 +485,7 @@ export class Element extends Node {
460485
if (node) {
461486
node.value = value
462487
} else {
463-
this.attributes.push({ localName: name, namespaceURI: null, ownerElement: this, value })
488+
this.attributes._list.push({ localName: name, namespaceURI: null, ownerElement: this, value })
464489
}
465490
}
466491

@@ -579,13 +604,13 @@ export class HTMLDialogElement extends HTMLElement { localName = 'dialog' }
579604
export class HTMLDivElement extends HTMLElement { localName = 'div' }
580605

581606
export class HTMLFieldSetElement extends HTMLElement {
582-
elements = []
607+
elements = new HTMLCollection
583608
localName = 'fieldset'
584609
type = 'fieldset'
585610
}
586611

587612
export class HTMLFormElement extends HTMLElement {
588-
elements = []
613+
elements = new HTMLCollection
589614
localName = 'form'
590615
}
591616

test/match.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,12 @@ describe('selector', () => {
370370
* @returns {string}
371371
*/
372372
function serializeElement({ attributes, localName }) {
373-
return `${attributes.reduce(
374-
(string, { localName, value }) =>
375-
string += ` ${value ? `${localName}="${value}"` : localName}`,
376-
`<${localName.toLowerCase()}`)}>`
373+
let string = `<${localName.toLowerCase()}`
374+
for (let index = 0; index < attributes.length; index++) {
375+
string += ` ${value ? `${localName}="${value}"` : localName}`
376+
}
377+
string += '>'
378+
return string
377379
}
378380

379381
class CSSAssert extends Assert {

0 commit comments

Comments
 (0)