Hello
30 |Hey
34 | `); 35 | 36 | const results = getNodesInCurrentScope( 37 | document, 38 | '.c-entry-content > div:has(> .e-image)' 39 | ); 40 | 41 | expect(results.length).toBe(1); 42 | 43 | const div =Hello
55 |Hi there60 |
Hey { 68 | const selector = '.c-entry-content > div:has(.e-image)'; 69 | const nodes = getNodesInCurrentScope(document, selector); 70 | const hasSelector = getHasInnerSelector(selector); 71 | const filteredNodes = filterNodesInScopeByHasSelector( 72 | nodes, 73 | hasSelector as string 74 | ); 75 | 76 | expect(filteredNodes.length).toBe(1); 77 | }); 78 | 79 | it('filters by direct selectors', () => { 80 | const selector = '.c-entry-content > div:has(> .e-image)'; 81 | const nodes = getNodesInCurrentScope(document, selector); 82 | const hasSelector = getHasInnerSelector(selector); 83 | const filteredNodes = filterNodesInScopeByHasSelector( 84 | nodes, 85 | hasSelector as string 86 | ); 87 | 88 | expect(filteredNodes.length).toBe(1); 89 | }); 90 | }); 91 | 92 | describe('querySelectorAllWithHas', () => { 93 | let document; 94 | 95 | beforeEach(() => { 96 | const { window } = new JSDOM(` 97 | 98 |
Hello
100 |Hi there105 |
Hey { 113 | const results = querySelectorAllWithHas( 114 | '.c-entry-content > div:has(> .e-image)', 115 | document 116 | ); 117 | 118 | expect(results.length).toBe(1); 119 | }); 120 | 121 | it('works like normal without has selector', () => { 122 | const results = querySelectorAllWithHas('.c-entry-content > p', document); 123 | 124 | expect(results.length).toBe(2); 125 | }); 126 | 127 | it('works like MDN docs prescribe', () => { 128 | const { window } = new JSDOM(` 129 | 130 |
Hello
132 | 133 |Hi there140 |
Hey img)', document);
147 |
148 | expect(results.length).toBe(1);
149 | });
150 | });
151 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "polyfill-css-has",
3 | "version": "1.0.0",
4 | "main": "./dist/polyfill-css-has.js",
5 | "types": "./dist/polyfill-css-has.d.ts",
6 | "license": "MIT",
7 | "engines": {
8 | "node": "^8.6.0"
9 | },
10 | "scripts": {
11 | "dev": "webpack --mode development",
12 | "build": "webpack --mode production",
13 | "test": "jest"
14 | },
15 | "devDependencies": {
16 | "@types/jest": "^22.2.3",
17 | "babel-core": "^6.26.0",
18 | "babel-loader": "^7.1.4",
19 | "babel-preset-env": "^1.6.1",
20 | "jest": "^22.4.3",
21 | "jsdom": "^11.9.0",
22 | "prettier": "^1.12.1",
23 | "ts-jest": "^22.4.4",
24 | "ts-loader": "^4.2.0",
25 | "typescript": "^2.8.3",
26 | "webpack": "^4.6.0",
27 | "webpack-cli": "^2.0.15"
28 | },
29 | "jest": {
30 | "transform": {
31 | "^.+\\.tsx?$": "ts-jest"
32 | },
33 | "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
34 | "moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"]
35 | },
36 | "dependencies": {
37 | "core-js": "^2.5.5"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/polyfill-css-has.ts:
--------------------------------------------------------------------------------
1 | import 'core-js/fn/array/from';
2 |
3 | /**
4 | * Perform querySelectorAll using the experimental :has() selector, as
5 | * defined by MDN.
6 | *
7 | * @example
8 | * const results = querySelectorAll('.container > div:has(> img)');
9 | *
10 | * @see https://developer.mozilla.org/en-US/docs/Web/CSS/:has
11 | * @param selector CSS selector containing :has()
12 | * @param dom Optional element to search. Defaults to document.
13 | */
14 | export default function querySelectorAllWithHas(
15 | selector: string,
16 | dom?: Element
17 | ): Node[] {
18 | const node = dom || document;
19 |
20 | const hasSelector = getHasInnerSelector(selector);
21 |
22 | if (!hasSelector) {
23 | return Array.from(node.querySelectorAll(selector));
24 | }
25 |
26 | const nodes = getNodesInCurrentScope(node, selector);
27 |
28 | return filterNodesInScopeByHasSelector(nodes, hasSelector as string);
29 | }
30 |
31 | /**
32 | * Get the inner-selector from the :has() statement.
33 | * Returns false if no :has() is present.
34 | *
35 | * @param selector A CSS selector possibly containing :has()
36 | */
37 | export function getHasInnerSelector(selector: string): string | Boolean {
38 | const matches = /:has\((.*)\)/.exec(selector);
39 |
40 | if (!matches) {
41 | return false;
42 | }
43 |
44 | return matches[1];
45 | }
46 |
47 | /**
48 | * Get the elements in the resolved scope prior to the :has() statement.
49 | *
50 | * @param dom Element
51 | * @param selector Selector
52 | */
53 | export function getNodesInCurrentScope(
54 | dom: Element | Document,
55 | selector: string
56 | ): NodeList {
57 | const currentScopeSelector = getCurrentScopeSelector(selector);
58 |
59 | return dom.querySelectorAll(currentScopeSelector);
60 | }
61 |
62 | /**
63 | * Grab the top-level scope, immediately to the left of :has()
64 | *
65 | * @param selector
66 | */
67 | function getCurrentScopeSelector(selector: string): string {
68 | return selector.slice(0, selector.indexOf(':has('));
69 | }
70 |
71 | /**
72 | * Perform the querySelectorAll behavior against the nodes at the top level.
73 | *
74 | * @param nodes Filtered nodes from the prior scope.
75 | * @param selector The inner :has() selector
76 | */
77 | export function filterNodesInScopeByHasSelector(
78 | nodes: NodeList,
79 | selector: string
80 | ): Node[] {
81 | let method: Function;
82 |
83 | method = selectorHasDirectDescendant(selector)
84 | ? filterNodeWithDirectDescendants
85 | : filterNode;
86 |
87 | return Array.from(nodes).filter(node => method(