oxios-web 0.2.0

Web dashboard channel for Oxios
# DOM Selector

[![build](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/node.js.yml)
[![CodeQL](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/asamuzaK/domSelector/actions/workflows/github-code-scanning/codeql)
[![npm (scoped)](https://img.shields.io/npm/v/@asamuzakjp/dom-selector)](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]&lt;([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&nbsp;F|| |
|E > F|| |
|E + F|| |
|E ~ F|| |
|F \|\| E|Unsupported| |
|E.warning|| |
|E#myid|| |
|E\[foo\]|| |
|E\[foo="bar"\]|| |
|E\[foo="bar"&nbsp;i\]|| |
|E\[foo="bar"&nbsp;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&#8209;link|| |
|E:link|| |
|E:visited||Returns `false` or `null` to prevent fingerprinting.|
|E:local&#8209;link|| |
|E:target|| |
|E:target&#8209;within|| |
|E:scope|| |
|E:hover|| |
|E:active|| |
|E:focus|| |
|E:focus&#8209;visible|| |
|E:focus&#8209;within|| |
|E:current|Unsupported| |
|E:current(s)|Unsupported| |
|E:past|Unsupported| |
|E:future|Unsupported| |
|E:open<br>E:closed|Partially supported|Matching with &lt;select&gt;, e.g. `select:open`, is not supported.|
|E:popover-open|Unsupported| |
|E:enabled<br>E:disabled|| |
|E:read&#8209;write<br>E:read&#8209;only|| |
|E:placeholder&#8209;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&#8209;valid<br>E:user&#8209;invalid|Unsupported| |
|E:root|| |
|E:empty|| |
|E:nth&#8209;child(n&nbsp;[of&nbsp;S]?)|| |
|E:nth&#8209;last&#8209;child(n&nbsp;[of&nbsp;S]?)|| |
|E:first&#8209;child|| |
|E:last&#8209;child|| |
|E:only&#8209;child|| |
|E:nth&#8209;of&#8209;type(n)|| |
|E:nth&#8209;last&#8209;of&#8209;type(n)|| |
|E:first&#8209;of&#8209;type|| |
|E:last&#8209;of&#8209;type|| |
|E:only&#8209;of&#8209;type|| |
|E:nth&#8209;col(n)|Unsupported| |
|E:nth&#8209;last&#8209;col(n)|Unsupported| |
|CE:state(v)||*1|
|:host|| |
|:host(s)|| |
|:host(:state(v))||*1|
|:host:has(rs1, rs2, ...)|| |
|:host(s):has(rs1, rs2, ...)|| |
|:host&#8209;context(s)|| |
|:host&#8209;context(s):has(rs1, rs2, ...)|| |
|&amp;||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