summaryrefslogtreecommitdiff
path: root/source/helpers/select-has.ts
blob: 29dd6756f4aa241b510b9df981422607dc4663fe (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import {ParseSelector} from 'typed-query-selector/parser.js';

const _isHasSelectorSupported = globalThis.CSS?.supports('selector(:has(a))');
export const isHasSelectorSupported = (): boolean => _isHasSelectorSupported;

// Adapted from https://stackoverflow.com/a/35271017/288906
const hasSelectorRegex = /:has\(((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*)\)/;

export default function selectHas<Selector extends string, ExpectedElement extends HTMLElement = ParseSelector<Selector, HTMLElement>>(selectors: Selector | Selector[], baseElement: ParentNode = document): ExpectedElement | void {
	const count = [...String(selectors).matchAll(/has\(/g)].length;
	if (count !== 1) { // Only one :has allowed. KISS
		throw new Error(`Only one \`:has()\` required/allowed, found ${count}`);
	}

	const parts = String(selectors).split(hasSelectorRegex);

	const [baseSelector, hasSelector, finalSelector] = parts;
	if (['', '*'].includes(baseSelector.trim())) {
		throw new Error('* is super inefficient in :has()');
	}

	if (/\s$/.test(baseSelector)) {
		throw new Error('No spaces before :has() supported');
	}

	if (/^[+~]/.test(hasSelector.trim())) {
		throw new Error('This polyfill only supports looking into the children of the base element');
	}

	for (const base of baseElement.querySelectorAll<ExpectedElement>(baseSelector)) {
		if (base.querySelector(':scope ' + hasSelector)) {
			if (!finalSelector.trim()) {
				return base;
			}

			const finalElement = base.querySelector<ExpectedElement>(finalSelector);
			if (finalElement) {
				return finalElement;
			}
		}
	}
}