DOM Selector
A CSS selector engine.
Install
npm i @asamuzakjp/dom-selector
Usage
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const { window } = new JSDOM();
const {
closest, matches, querySelector, querySelectorAll
} = new DOMSelector(window);
matches(selector, node, opt)
matches - equivalent to Element.matches()
Parameters
selector
string CSS selectornode
object Element nodeopt
object? options
opt.noexcept
boolean? no exceptionopt.warn
boolean? console warn e.g. unsupported pseudo-class
Returns boolean true
if matched, false
otherwise
closest(selector, node, opt)
closest - equivalent to Element.closest()
Parameters
selector
string CSS selectornode
object Element nodeopt
object? options
opt.noexcept
boolean? no exceptionopt.warn
boolean? console warn e.g. unsupported pseudo-class
Returns object? matched node
querySelector(selector, node, opt)
querySelector - equivalent to Document.querySelector(), DocumentFragment.querySelector() and Element.querySelector()
Parameters
selector
string CSS selectornode
object Document, DocumentFragment or Element nodeopt
object? options
opt.noexcept
boolean? no exceptionopt.warn
boolean? console warn e.g. unsupported pseudo-class
Returns object? matched node
querySelectorAll(selector, node, opt)
querySelectorAll - equivalent to Document.querySelectorAll(), DocumentFragment.querySelectorAll() and Element.querySelectorAll()
NOTE: returns Array, not NodeList
Parameters
selector
string CSS selectornode
object Document, DocumentFragment or Element nodeopt
object? options
opt.noexcept
boolean? no exceptionopt.warn
boolean? console warn e.g. unsupported pseudo-class
Returns Array<(object | undefined)> array of matched nodes
Monkey patch jsdom
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const dom = new JSDOM('', {
runScripts: 'dangerously',
url: 'http://localhost/',
beforeParse: window => {
const domSelector = new DOMSelector(window);
const matches = domSelector.matches.bind(domSelector);
window.Element.prototype.matches = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return matches(selector, this);
};
const closest = domSelector.closest.bind(domSelector);
window.Element.prototype.closest = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return closest(selector, this);
};
const querySelector = domSelector.querySelector.bind(domSelector);
window.Document.prototype.querySelector = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelector(selector, this);
};
window.DocumentFragment.prototype.querySelector = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelector(selector, this);
};
window.Element.prototype.querySelector = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelector(selector, this);
};
const querySelectorAll = domSelector.querySelectorAll.bind(domSelector);
window.Document.prototype.querySelectorAll = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelectorAll(selector, this);
};
window.DocumentFragment.prototype.querySelectorAll = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelectorAll(selector, this);
};
window.Element.prototype.querySelectorAll = function (...args) {
if (!args.length) {
throw new window.TypeError('1 argument required, but only 0 present.');
}
const [selector] = args;
return querySelectorAll(selector, this);
};
}
});
Supported CSS selectors
Pattern | Supported | Note |
---|
* | ✓ | |
ns|E | ✓ | |
*|E | ✓ | |
|E | ✓ | |
E | ✓ | |
E:not(s1, s2, …) | ✓ | |
E:is(s1, s2, …) | ✓ | |
E:where(s1, s2, …) | ✓ | |
E:has(rs1, rs2, …) | ✓ | |
E.warning | ✓ | |
E#myid | ✓ | |
E[foo] | ✓ | |
E[foo="bar"] | ✓ | |
E[foo="bar" i] | ✓ | |
E[foo="bar" s] | ✓ | |
E[foo~="bar"] | ✓ | |
E[foo^="bar"] | ✓ | |
E[foo$="bar"] | ✓ | |
E[foo*="bar"] | ✓ | |
E[foo|="en"] | ✓ | |
E:defined | Partially supported | Matching with MathML is not yet supported. |
E:dir(ltr) | ✓ | |
E:lang(en) | ✓ | |
E:any‑link | ✓ | |
E:link | ✓ | |
E:visited | ✓ | Returns false or null to prevent fingerprinting. |
E:local‑link | ✓ | |
E:target | ✓ | |
E:target‑within | ✓ | |
E:scope | ✓ | |
E:current | Unsupported | |
E:current(s) | Unsupported | |
E:past | Unsupported | |
E:future | Unsupported | |
E:active | ✓ | |
E:hover | ✓ | |
E:focus | ✓ | |
E:focus‑within | ✓ | |
E:focus‑visible | ✓ | |
E:open E:closed | Partially supported | Matching with <select>, e.g. select:open , is not supported. |
E:enabled E:disabled | ✓ | |
E:read‑write E:read‑only | ✓ | |
E:placeholder‑shown | ✓ | |
E:default | ✓ | |
E:checked | ✓ | |
E:indeterminate | ✓ | |
E:valid E:invalid | ✓ | |
E:required E:optional | ✓ | |
E:blank | Unsupported | |
E:user‑valid E:user‑invalid | Unsupported | |
E:root | ✓ | |
E:empty | ✓ | |
E:nth‑child(n [of S]?) | ✓ | |
E:nth‑last‑child(n [of S]?) | ✓ | |
E:first‑child | ✓ | |
E:last‑child | ✓ | |
E:only‑child | ✓ | |
E:nth‑of‑type(n) | ✓ | |
E:nth‑last‑of‑type(n) | ✓ | |
E:first‑of‑type | ✓ | |
E:last‑of‑type | ✓ | |
E:only‑of‑type | ✓ | |
E F | ✓ | |
E > F | ✓ | |
E + F | ✓ | |
E ~ F | ✓ | |
F || E | Unsupported | |
E:nth‑col(n) | Unsupported | |
E:nth‑last‑col(n) | Unsupported | |
E:popover-open | ✓ | |
E:state(v) | ✓ | *1 |
:host | ✓ | |
:host(s) | ✓ | |
:host‑context(s) | ✓ | |
:host(:state(v)) | ✓ | *1 |
:host:has(rs1, rs2, ...) | ✓ | |
:host(s):has(rs1, rs2, ...) | ✓ | |
:host‑context(s):has(rs1, rs2, ...) | ✓ | |
*1: ElementInternals.states
, i.e. CustomStateSet
, is not implemented in jsdom, so you need to apply a patch in the custom element constructor.
class LabeledCheckbox extends window.HTMLElement {
#internals;
constructor() {
super();
this.#internals = this.attachInternals();
if (!this.#internals.states) {
this.#internals.states = new Set();
}
this.addEventListener('click', this._onClick.bind(this));
}
get checked() {
return this.#internals.states.has('checked');
}
set checked(flag) {
if (flag) {
this.#internals.states.add('checked');
} else {
this.#internals.states.delete('checked');
}
}
_onClick(event) {
this.checked = !this.checked;
}
}
Performance
See benchmark for the latest results.
F
: Failed because the selector is not supported or the result was incorrect.
matches()
Selector | jsdom v25.0.1 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|
simple selector:
matches('.content') | 970,195 ops/sec ±1.09% | 6,434,629 ops/sec ±0.72% | 8,542 ops/sec ±0.55% | 759,550 ops/sec ±0.36% | happydom is the fastest and 8.5 times faster than patched-jsdom. jsdom is 1.3 times faster than patched-jsdom. |
compound selector:
matches('p.content[id]:is(:last-child, :only-child)') | 577,536 ops/sec ±1.66% | 5,548,367 ops/sec ±1.02% | 8,290 ops/sec ±0.73% | 418,490 ops/sec ±0.46% | happydom is the fastest and 13.3 times faster than patched-jsdom. jsdom is 1.4 times faster than patched-jsdom. |
compound selector:
matches('p.content[id]:is(:invalid-nth-child, :only-child)') | F | 4,987,842 ops/sec ±1.99% | F | 88,901 ops/sec ±1.31% | happydom is the fastest and 56.1 times faster than patched-jsdom. |
compound selector:
matches('p.content[id]:not(:is(.foo, .bar))') | 463,747 ops/sec ±1.81% | 5,014,172 ops/sec ±0.53% | 8,200 ops/sec ±0.91% | 334,734 ops/sec ±1.63% | happydom is the fastest and 15.0 times faster than patched-jsdom. jsdom is 1.4 times faster than patched-jsdom. |
complex selector:
matches('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') | 145,755 ops/sec ±2.56% | F | 5,309 ops/sec ±1.61% | 122,806 ops/sec ±0.33% | jsdom is the fastest and 1.2 times faster than patched-jsdom. |
complex selector:
matches('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner:has(> .content)') | F | F | 5,372 ops/sec ±0.85% | 18,921 ops/sec ±1.80% | patched-jsdom is the fastest. |
complex selector within logical pseudo-class:
matches(':is(.box > .content, .block > .content)') | 394,756 ops/sec ±1.62% | F | 5,688 ops/sec ±0.65% | 316,784 ops/sec ±1.25% | jsdom is the fastest and 1.2 times faster than patched-jsdom. |
closest()
Selector | jsdom v25.0.1 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|
simple selector:
closest('.container') | 364,468 ops/sec ±0.84% | 2,002,932 ops/sec ±0.94% | 8,666 ops/sec ±0.76% | 328,519 ops/sec ±1.61% | happydom is the fastest and 6.1 times faster than patched-jsdom. jsdom is 1.1 times faster than patched-jsdom. |
compound selector:
closest('div.container[id]:not(.foo, .box)') | 130,479 ops/sec ±1.60% | F | 8,186 ops/sec ±0.71% | 118,992 ops/sec ±1.59% | jsdom is the fastest and 1.1 times faster than patched-jsdom. |
complex selector:
closest('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') | 139,006 ops/sec ±1.51% | F | 5,315 ops/sec ±0.71% | 114,474 ops/sec ±1.29% | jsdom is the fastest and 1.2 times faster than patched-jsdom. |
complex selector:
closest('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner:has(> .content)') | F | F | 5,228 ops/sec ±0.40% | 15,264 ops/sec ±1.96% | patched-jsdom is the fastest. |
complex selector within logical pseudo-class:
closest(':is(.container > .content, .container > .box)') | 186,594 ops/sec ±1.66% | 1,735,355 ops/sec ±1.03% | 5,608 ops/sec ±0.72% | 166,431 ops/sec ±0.24% | happydom is the fastest and 10.4 times faster than patched-jsdom. jsdom is 1.1 times faster than patched-jsdom. |
querySelector()
Selector | jsdom v25.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|
simple selector:
querySelector('.content') | 29,510 ops/sec ±0.88% | 3,177,172 ops/sec ±1.25% | 10,026 ops/sec ±0.83% | 29,689 ops/sec ±1.43% | happydom is the fastest and 107.0 times faster than patched-jsdom. patched-jsdom is 1.0 times faster than jsdom. |
compound selector:
querySelector('p.content[id]:is(:last-child, :only-child)') | 9,909 ops/sec ±1.42% | 2,754,183 ops/sec ±1.03% | 9,476 ops/sec ±0.59% | 9,355 ops/sec ±1.40% | happydom is the fastest and 294.4 times faster than patched-jsdom. jsdom is 1.1 times faster than patched-jsdom. |
complex selector:
querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') | 217 ops/sec ±1.98% | F | 1,402 ops/sec ±2.03% | 444 ops/sec ±1.64% | linkedom is the fastest and 3.2 times faster than patched-jsdom. patched-jsdom is 2.0 times faster than jsdom. |
complex selector:
querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner:has(> .content)') | F | F | 1,619 ops/sec ±0.62% | 320 ops/sec ±1.49% | linkedom is the fastest and 5.1 times faster than patched-jsdom. |
complex selector within logical pseudo-class:
querySelector(':is(.box > .content, .block > .content)') | 3,180 ops/sec ±1.84% | F | 9,415 ops/sec ±0.69% | 208,961 ops/sec ±2.48% | patched-jsdom is the fastest. patched-jsdom is 65.7 times faster than jsdom. |
querySelectorAll()
Selector | jsdom v25.0.1 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|
simple selector:
querySelectorAll('.content') | 2,892 ops/sec ±1.11% | 5,500,951 ops/sec ±0.94% | 1,218 ops/sec ±0.25% | 3,244 ops/sec ±1.25% | happydom is the fastest and 1695.8 times faster than patched-jsdom. patched-jsdom is 1.1 times faster than jsdom. |
compound selector:
querySelectorAll('p.content[id]:is(:last-child, :only-child)') | 964 ops/sec ±1.18% | 4,385,931 ops/sec ±0.80% | 1,193 ops/sec ±1.17% | ,020 ops/sec ±0.14% | happydom is the fastest and 4299.2 times faster than patched-jsdom. patched-jsdom is 1.1 times faster than jsdom. |
complex selector:
querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') | 218 ops/sec ±1.53% | F | 429 ops/sec ±1.71% | 479 ops/sec ±1.20% | patched-jsdom is the fastest. patched-jsdom is 2.2 times faster than jsdom. |
complex selector:
querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner:has(> .content)') | F | F | 465 ops/sec ±0.18% | 332 ops/sec ±1.21% | linkedom is the fastest and 1.4 times faster than patched-jsdom. |
complex selector within logical pseudo-class:
querySelectorAll(':is(.box > .content, .block > .content)') | 302 ops/sec ±1.50% | F | 542 ops/sec ±0.33% | 795 ops/sec ±3.24% | patched-jsdom is the fastest. patched-jsdom is 2.6 times faster than jsdom. |
Acknowledgments
The following resources have been of great help in the development of the DOM Selector.
Copyright (c) 2023 asamuzaK (Kazz)