A CSS selector engine.
npm i @asamuzakjp/dom-selector
import { DOMSelector } from '@asamuzakjp/dom-selector';
import { JSDOM } from 'jsdom';
const { window } = new JSDOM();
const {
closest, matches, querySelector, querySelectorAll
} = new DOMSelector(window);
matches - equivalent to Element.matches()
Returns boolean true
if matched, false
otherwise
closest - equivalent to Element.closest()
Returns object? matched node
querySelector - equivalent to Document.querySelector(), DocumentFragment.querySelector() and Element.querySelector()
selector
string CSS selectornode
object Document, DocumentFragment or Element nodeopt
object? options
Returns object? matched node
querySelectorAll - equivalent to Document.querySelectorAll(), DocumentFragment.querySelectorAll() and Element.querySelectorAll()
NOTE: returns Array, not NodeList
selector
string CSS selectornode
object Document, DocumentFragment or Element nodeopt
object? options
Returns Array<(object | undefined)> array of matched nodes
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);
};
}
});
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, ...) | ✓ | |
& | ✓ | Only supports outermost & , i.e. equivalent to :scope |
*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();
// patch CustomStateSet
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;
}
}
See benchmark for the latest results.
F
: Failed because the selector is not supported or the result was incorrect.
Selector | jsdom v26.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|---|---|---|---|---|
simple selector:matches('.content') |
112,297 ops/sec ±4.41% | 403,800 ops/sec ±2.26% | 9,352 ops/sec ±1.18% | 109,587 ops/sec ±0.36% | happydom is the fastest and 3.7 times faster than patched-jsdom. jsdom is 1.0 times faster than patched-jsdom. |
compound selector:matches('p.content[id]:is(:last-child, :only-child)') |
102,156 ops/sec ±0.83% | 403,804 ops/sec ±0.91% | 9,051 ops/sec ±0.78% | 77,568 ops/sec ±4.06% | happydom is the fastest and 5.2 times faster than patched-jsdom. jsdom is 1.3 times faster than patched-jsdom. |
compound selector:matches('p.content[id]:is(:invalid-nth-child, :only-child)') |
F | 387,515 ops/sec ±4.34% | F | 41,904 ops/sec ±1.57% | happydom is the fastest and 9.2 times faster than patched-jsdom. |
compound selector:matches('p.content[id]:not(:is(.foo, .bar))') |
97,333 ops/sec ±0.62% | 394,134 ops/sec ±2.82% | 8,821 ops/sec ±0.86% | 79,436 ops/sec ±0.69% | happydom is the fastest and 5.0 times faster than patched-jsdom. jsdom is 1.2 times faster than patched-jsdom. |
complex selector:matches('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') |
63,065 ops/sec ±0.95% | F | 5,751 ops/sec ±0.73% | 57,917 ops/sec ±1.27% | jsdom is the fastest and 1.1 times faster than patched-jsdom. |
complex selector:matches('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner:has(> .content)') |
29,609 ops/sec ±1.68% | F | 5,741 ops/sec ±1.12% | 39,522 ops/sec ±3.55% | patched-jsdom is the fastest. patched-jsdom is 1.3 times faster than jsdom. |
complex selector within logical pseudo-class:matches(':is(.box > .content, .block > .content)') |
89,634 ops/sec ±0.66% | F | 6,049 ops/sec ±0.33% | 83,881 ops/sec ±0.70% | jsdom is the fastest and 1.1 times faster than patched-jsdom. |
nested and chained :not() selector:matches('p:not(:is(:not(.content))):not(.foo)') |
F | 377,155 ops/sec ±1.07% | 6,087 ops/sec ±0.70% | 60,032 ops/sec ±26.27% | happydom is the fastest and 6.3 times faster than patched-jsdom. |
Selector | jsdom v26.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|---|---|---|---|---|
simple selector:closest('.container') |
68,204 ops/sec ±1.82% | 266,567 ops/sec ±40.99% | 9,268 ops/sec ±0.81% | 82,517 ops/sec ±0.79% | happydom is the fastest and 3.2 times faster than patched-jsdom. patched-jsdom is 1.2 times faster than jsdom. |
compound selector:closest('div.container[id]:not(.foo, .box)') |
60,091 ops/sec ±0.56% | F | 8,418 ops/sec ±1.06% | 51,243 ops/sec ±0.74% | 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 > .content') |
59,694 ops/sec ±0.61% | F | 5,801 ops/sec ±0.69% | 53,569 ops/sec ±0.65% | 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:has(> .content)') |
22,696 ops/sec ±0.97% | F | 5,800 ops/sec ±0.52% | 34,907 ops/sec ±1.00% | patched-jsdom is the fastest. patched-jsdom is 1.5 times faster than jsdom. |
complex selector within logical pseudo-class:closest(':is(.container > .content, .container > .box)') |
68,572 ops/sec ±0.55% | 338,382 ops/sec ±1.46% | 5,985 ops/sec ±1.24% | 64,454 ops/sec ±0.49% | happydom is the fastest and 5.2 times faster than patched-jsdom. jsdom is 1.1 times faster than patched-jsdom. |
nested and chained :not() selector:closest('div:not(:is(:not(.container))):not(.box)') |
F | F | 8,638 ops/sec ±0.52% | 55,838 ops/sec ±34.28% | patched-jsdom is the fastest. |
Selector | jsdom v26.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|---|---|---|---|---|
simple selector:querySelector('.content') |
15,912 ops/sec ±2.11% | 307,120 ops/sec ±26.93% | 10,595 ops/sec ±0.99% | 71,245 ops/sec ±1.77% | happydom is the fastest and 4.3 times faster than patched-jsdom. patched-jsdom is 4.5 times faster than jsdom. |
compound selector:querySelector('p.content[id]:is(:last-child, :only-child)') |
7,333 ops/sec ±0.86% | 290,078 ops/sec ±42.38% | 9,955 ops/sec ±1.82% | 34,346 ops/sec ±1.15% | happydom is the fastest and 8.4 times faster than patched-jsdom. patched-jsdom is 4.7 times faster than jsdom. |
complex selector:querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content') |
123 ops/sec ±1.40% | F | 1,021 ops/sec ±0.53% | 553 ops/sec ±1.99% | linkedom is the fastest and 1.8 times faster than patched-jsdom. patched-jsdom is 4.5 times faster than jsdom. |
complex selector:querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner:has(> .content)') |
40.29 ops/sec ±6.51% | F | 1,304 ops/sec ±0.87% | 382 ops/sec ±1.12% | linkedom is the fastest and 3.4 times faster than patched-jsdom. patched-jsdom is 9.5 times faster than jsdom. |
complex selector within logical pseudo-class:querySelector(':is(.box > .content, .block > .content)') |
2,104 ops/sec ±0.69% | F | 9,762 ops/sec ±0.68% | 67,358 ops/sec ±0.69% | patched-jsdom is the fastest. patched-jsdom is 32.0 times faster than jsdom. |
nested and chained :not() selector:querySelector('p:not(:is(:not(.content))):not(.foo)') |
F | 354,191 ops/sec ±1.43% | 9,950 ops/sec ±0.86% | 65,426 ops/sec ±0.91% | happydom is the fastest and 5.4 times faster than patched-jsdom. |
Selector | jsdom v26.0.0 (nwsapi) | happy-dom | linkeDom | patched-jsdom (dom-selector) | Result |
---|---|---|---|---|---|
simple selector:querySelectorAll('.content') |
938 ops/sec ±1.61% | 285 ops/sec ±3.27% | 993 ops/sec ±2.52% | 794 ops/sec ±37.70% | linkedom is the fastest and 1.3 times faster than patched-jsdom. jsdom is 1.2 times faster than patched-jsdom. |
compound selector:querySelectorAll('p.content[id]:is(:last-child, :only-child)') |
331 ops/sec ±2.79% | 320 ops/sec ±4.68% | 949 ops/sec ±0.89% | 354 ops/sec ±26.10% | linkedom is the fastest and 2.7 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') |
125 ops/sec ±0.67% | F | 298 ops/sec ±0.85% | 140 ops/sec ±1.03% | linkedom is the fastest and 2.1 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:has(> .content)') |
42.34 ops/sec ±0.89% | F | 351 ops/sec ±0.89% | 135 ops/sec ±1.36% | linkedom is the fastest and 2.6 times faster than patched-jsdom. patched-jsdom is 3.2 times faster than jsdom. |
complex selector within logical pseudo-class:querySelectorAll(':is(.box > .content, .block > .content)') |
161 ops/sec ±1.07% | F | 369 ops/sec ±1.11% | 720 ops/sec ±1.43% | patched-jsdom is the fastest. patched-jsdom is 4.5 times faster than jsdom. |
nested and chained :not() selector:querySelectorAll('p:not(:is(:not(.content))):not(.foo)') |
F | 296 ops/sec ±46.31% | 998 ops/sec ±0.79% | 1,168 ops/sec ±1.12% | patched-jsdom is the fastest. |
The following resources have been of great help in the development of the DOM Selector.
Copyright (c) 2023 asamuzaK (Kazz)