# DOM Selector
[](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
[](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql)
[](https://www.npmjs.com/package/@asamuzakjp/dom-selector)
A CSS selector engine.
## Install
```console
npm i @asamuzakjp/dom-selector
```
## Usage
```javascript
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()][64]
#### Parameters
- `selector` **[string][59]** CSS selector
- `node` **[object][60]** Element node
- `opt` **[object][60]?** options
- `opt.noexcept` **[boolean][61]?** no exception
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
Returns **[boolean][61]** `true` if matched, `false` otherwise
### closest(selector, node, opt)
closest - equivalent to [Element.closest()][65]
#### Parameters
- `selector` **[string][59]** CSS selector
- `node` **[object][60]** Element node
- `opt` **[object][60]?** options
- `opt.noexcept` **[boolean][61]?** no exception
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
Returns **[object][60]?** matched node
### querySelector(selector, node, opt)
querySelector - equivalent to [Document.querySelector()][66], [DocumentFragment.querySelector()][67] and [Element.querySelector()][68]
#### Parameters
- `selector` **[string][59]** CSS selector
- `node` **[object][60]** Document, DocumentFragment or Element node
- `opt` **[object][60]?** options
- `opt.noexcept` **[boolean][61]?** no exception
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
Returns **[object][60]?** matched node
### querySelectorAll(selector, node, opt)
querySelectorAll - equivalent to [Document.querySelectorAll()][69], [DocumentFragment.querySelectorAll()][70] and [Element.querySelectorAll()][71]
**NOTE**: returns Array, not NodeList
#### Parameters
- `selector` **[string][59]** CSS selector
- `node` **[object][60]** Document, DocumentFragment or Element node
- `opt` **[object][60]?** options
- `opt.noexcept` **[boolean][61]?** no exception
- `opt.warn` **[boolean][61]?** console warn e.g. unsupported pseudo-class
Returns **[Array][62]<([object][60] \| [undefined][63])>** array of matched nodes
## Monkey patch jsdom
``` javascript
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|
|:--------|:-------:|:--------|
|\*|✓| |
|E|✓| |
|ns\|E|✓| |
|\*\|E|✓| |
|\|E|✓| |
|E F|✓| |
|E > F|✓| |
|E + F|✓| |
|E ~ F|✓| |
|F \|\| E|Unsupported| |
|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:is(s1, s2, …)|✓| |
|E:not(s1, s2, …)|✓| |
|E:where(s1, s2, …)|✓| |
|E:has(rs1, rs2, …)|✓| |
|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:hover|✓| |
|E:active|✓| |
|E:focus|✓| |
|E:focus‑visible|✓| |
|E:focus‑within|✓| |
|E:current|Unsupported| |
|E:current(s)|Unsupported| |
|E:past|Unsupported| |
|E:future|Unsupported| |
|E:open
E:closed|Partially supported|Matching with <select>, e.g. `select:open`, is not supported.|
|E:popover-open|✓| |
|E:enabled
E:disabled|✓| |
|E:read‑write
E:read‑only|✓| |
|E:placeholder‑shown|✓| |
|E:default|✓| |
|E:checked|✓| |
|E:indeterminate|✓| |
|E:blank|Unsupported| |
|E:valid
E:invalid|✓| |
|E:in-range
E:out-of-range|✓| |
|E:required
E:optional|✓| |
|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:nth‑col(n)|Unsupported| |
|E:nth‑last‑col(n)|Unsupported| |
|CE:state(v)|✓|*1|
|:host|✓| |
|:host(s)|✓| |
|:host(:state(v))|✓|*1|
|:host:has(rs1, rs2, ...)|✓| |
|:host(s):has(rs1, rs2, ...)|✓| |
|:host‑context(s)|✓| |
|: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.
``` javascript
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](https://github.com/asamuzaK/domSelector/actions/workflows/benchmark.yml) for the latest results.
## Acknowledgments
The following resources have been of great help in the development of the DOM Selector.
- [CSSTree](https://github.com/csstree/csstree)
- [selery](https://github.com/danburzo/selery)
- [jsdom](https://github.com/jsdom/jsdom)
- [nwsapi](https://github.com/dperini/nwsapi)
---
Copyright (c) 2023 [asamuzaK (Kazz)](https://github.com/asamuzaK/)
[1]: #matches
[2]: #parameters
[3]: #closest
[4]: #parameters-1
[5]: #queryselector
[6]: #parameters-2
[7]: #queryselectorall
[8]: #parameters-3
[59]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[60]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[61]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[62]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[63]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/undefined
[64]: https://developer.mozilla.org/docs/Web/API/Element/matches
[65]: https://developer.mozilla.org/docs/Web/API/Element/closest
[66]: https://developer.mozilla.org/docs/Web/API/Document/querySelector
[67]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelector
[68]: https://developer.mozilla.org/docs/Web/API/Element/querySelector
[69]: https://developer.mozilla.org/docs/Web/API/Document/querySelectorAll
[70]: https://developer.mozilla.org/docs/Web/API/DocumentFragment/querySelectorAll
[71]: https://developer.mozilla.org/docs/Web/API/Element/querySelectorAll