@asamuzakjp/dom-selector
v6.3.7
Published
A CSS selector engine.
Downloads
1,442,477
Readme
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
Returns boolean true
if matched, false
otherwise
closest(selector, node, opt)
closest - equivalent to Element.closest()
Parameters
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
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
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:openE:closed|Partially supported|Matching with <select>, e.g. select:open
, is not supported.|
|E:enabledE:disabled|✓| |
|E:read‑writeE:read‑only|✓| |
|E:placeholder‑shown|✓| |
|E:default|✓| |
|E:checked|✓| |
|E:indeterminate|✓| |
|E:validE:invalid|✓| |
|E:requiredE:optional|✓| |
|E:blank|Unsupported| |
|E:user‑validE: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();
// 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;
}
}
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')
|115,054 ops/sec ±2.48%|356,979 ops/sec ±2.34%|8,556 ops/sec ±0.90%|118,715 ops/sec ±0.17%|happydom is the fastest and 3.0 times faster than patched-jsdom. patched-jsdom is 1.0 times faster than jsdom.|
|compound selector:matches('p.content[id]:is(:last-child, :only-child)')
|109,730 ops/sec ±0.63%|323,008 ops/sec ±18.02%|8,419 ops/sec ±0.66%|86,074 ops/sec ±0.97%|happydom is the fastest and 3.8 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|353,239 ops/sec ±2.73%|F|43,662 ops/sec ±1.86%|happydom is the fastest and 8.1 times faster than patched-jsdom.|
|compound selector:matches('p.content[id]:not(:is(.foo, .bar))')
|101,636 ops/sec ±1.59%|351,747 ops/sec ±0.59%|8,082 ops/sec ±0.61%|83,928 ops/sec ±0.66%|happydom is the fastest and 4.2 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')
|70,482 ops/sec ±0.38%|F|5,261 ops/sec ±0.72%|61,745 ops/sec ±0.56%|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)')
|31,671 ops/sec ±1.14%|F|5,288 ops/sec ±0.43%|42,614 ops/sec ±2.51%|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)')
|100,293 ops/sec ±0.67%|F|5,593 ops/sec ±0.40%|78,292 ops/sec ±26.38%|jsdom is the fastest and 1.3 times faster than patched-jsdom.|
|nested and chained :not() selector:matches('p:not(:is(:not(.content))):not(.foo)')
|F|299,143 ops/sec ±28.61%|5,538 ops/sec ±0.44%|84,061 ops/sec ±0.71%|happydom is the fastest and 3.6 times faster than patched-jsdom.|
closest()
|Selector|jsdom v25.0.1 (nwsapi)|happy-dom|linkeDom|patched-jsdom (dom-selector)|Result|
|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|
|simple selector:closest('.container')
|96,990 ops/sec ±0.60%|326,924 ops/sec ±0.52%|8,453 ops/sec ±0.65%|90,555 ops/sec ±1.70%|happydom is the fastest and 3.6 times faster than patched-jsdom. jsdom is 1.1 times faster than patched-jsdom.|
|compound selector:closest('div.container[id]:not(.foo, .box)')
|63,821 ops/sec ±0.74%|F|7,942 ops/sec ±0.47%|54,674 ops/sec ±0.83%|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')
|65,863 ops/sec ±0.65%|F|5,336 ops/sec ±0.55%|57,088 ops/sec ±0.76%|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)')
|23,941 ops/sec ±1.24%|F|5,182 ops/sec ±1.02%|36,066 ops/sec ±0.48%|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)')
|76,340 ops/sec ±0.36%|235,292 ops/sec ±34.48%|5,533 ops/sec ±0.53%|68,793 ops/sec ±0.78%|happydom is the fastest and 3.4 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|7,777 ops/sec ±0.65%|71,028 ops/sec ±0.55%|patched-jsdom is the fastest.|
querySelector()
|Selector|jsdom v25.0.1 (nwsapi)|happy-dom|linkeDom|patched-jsdom (dom-selector)|Result|
|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|
|simple selector:querySelector('.content')
|21,898 ops/sec ±1.84%|265,006 ops/sec ±42.03%|9,921 ops/sec ±0.56%|81,017 ops/sec ±1.27%|happydom is the fastest and 3.3 times faster than patched-jsdom. patched-jsdom is 3.7 times faster than jsdom.|
|compound selector:querySelector('p.content[id]:is(:last-child, :only-child)')
|8,676 ops/sec ±0.71%|327,427 ops/sec ±1.06%|9,507 ops/sec ±0.76%|37,303 ops/sec ±0.95%|happydom is the fastest and 8.8 times faster than patched-jsdom. patched-jsdom is 4.3 times faster than jsdom.|
|complex selector:querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content')
|197 ops/sec ±0.84%|F|1,230 ops/sec ±0.91%|616 ops/sec ±1.22%|linkedom is the fastest and 2.0 times faster than patched-jsdom. patched-jsdom is 3.1 times faster than jsdom.|
|complex selector:querySelector('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner:has(> .content)')
|57.15 ops/sec ±2.55%|F|1,452 ops/sec ±0.62%|453 ops/sec ±0.95%|linkedom is the fastest and 3.2 times faster than patched-jsdom. patched-jsdom is 7.9 times faster than jsdom.|
|complex selector within logical pseudo-class:querySelector(':is(.box > .content, .block > .content)')
|3,045 ops/sec ±1.16%|F|9,229 ops/sec ±0.76%|74,154 ops/sec ±0.83%|patched-jsdom is the fastest. patched-jsdom is 24.4 times faster than jsdom.|
|nested and chained :not() selector:querySelector('p:not(:is(:not(.content))):not(.foo)')
|F|323,503 ops/sec ±0.95%|9,383 ops/sec ±0.63%|68,866 ops/sec ±0.70%|happydom is the fastest and 4.7 times faster than patched-jsdom.|
querySelectorAll()
|Selector|jsdom v25.0.1 (nwsapi)|happy-dom|linkeDom|patched-jsdom (dom-selector)|Result|
|:-----------|:-----------|:-----------|:-----------|:-----------|:-----------|
|simple selector:querySelectorAll('.content')
|973 ops/sec ±49.13%|477 ops/sec ±43.91%|1,134 ops/sec ±0.38%|1,221 ops/sec ±0.69%|patched-jsdom is the fastest. patched-jsdom is 1.3 times faster than jsdom.|
|compound selector:querySelectorAll('p.content[id]:is(:last-child, :only-child)')
|681 ops/sec ±0.58%|572 ops/sec ±3.54%|1,124 ops/sec ±0.80%|640 ops/sec ±0.62%|linkedom is the fastest and 1.8 times faster than patched-jsdom. jsdom is 1.1 times faster than patched-jsdom.|
|complex selector:querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner > .content')
|198 ops/sec ±0.92%|F|386 ops/sec ±0.74%|206 ops/sec ±0.70%|linkedom is the fastest and 1.9 times faster than patched-jsdom. patched-jsdom is 1.0 times faster than jsdom.|
|complex selector:querySelectorAll('.box:first-child ~ .box:nth-of-type(4n+1) + .box[id] .block.inner:has(> .content)')
|56.19 ops/sec ±1.54%|F|419 ops/sec ±0.47%|183 ops/sec ±0.57%|linkedom is the fastest and 2.3 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)')
|276 ops/sec ±0.97%|F|468 ops/sec ±0.74%|811 ops/sec ±1.18%|patched-jsdom is the fastest. patched-jsdom is 2.9 times faster than jsdom.|
|nested and chained :not() selector:querySelectorAll('p:not(:is(:not(.content))):not(.foo)')
|F|523 ops/sec ±5.90%|1,160 ops/sec ±1.16%|1,321 ops/sec ±0.52%|patched-jsdom is the fastest.|
Acknowledgments
The following resources have been of great help in the development of the DOM Selector.
Copyright (c) 2023 asamuzaK (Kazz)