325 lines
10 KiB
Markdown
325 lines
10 KiB
Markdown
|
|
# 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);
|
||
|
|
```
|
||
|
|
|
||
|
|
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
|
||
|
|
|
||
|
|
### 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<br>E:closed|Partially supported|Matching with <select>, e.g. `select:open`, is not supported.|
|
||
|
|
|E:popover-open|✓| |
|
||
|
|
|E:enabled<br>E:disabled|✓| |
|
||
|
|
|E:read‑write<br>E:read‑only|✓| |
|
||
|
|
|E:placeholder‑shown|✓| |
|
||
|
|
|E:default|✓| |
|
||
|
|
|E:checked|✓| |
|
||
|
|
|E:indeterminate|✓| |
|
||
|
|
|E:blank|Unsupported| |
|
||
|
|
|E:valid<br>E:invalid|✓| |
|
||
|
|
|E:in-range<br>E:out-of-range|✓| |
|
||
|
|
|E:required<br>E:optional|✓| |
|
||
|
|
|E:user‑valid<br>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
|