Skip to content

Commit 27f32df

Browse files
committed
feat: add useScope option
refs #1061
1 parent cc780c3 commit 27f32df

File tree

6 files changed

+106
-6
lines changed

6 files changed

+106
-6
lines changed

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ In some cases, this selector may not be unique (e.g. `#wrapper > * > div > *`).
122122
- [`includeTag`](#include-tag)
123123
- [`maxCombinations`](#max-combinations)
124124
- [`maxCandidates`](#max-candidates)
125+
- [`useScope`](#use-scope)
125126
126127
### Selector types
127128
@@ -326,6 +327,75 @@ You should use it in cases, when there are not too many class names and attribut
326327
getCssSelector(targetElement, { maxCandidates: 100 });
327328
```
328329
330+
### Use scope
331+
332+
**Experimental feature** - *This will probably be turned on by default and the option will be removed, after I thoroughly evaluate that it produces valid selectors in all use cases.*
333+
334+
If set to `true` and the [`root` option](#root-element) is provided, the fallback selectors will be created relative to the `root` element using the `:scope` pseudo-class.
335+
336+
For example, if you have the following HTML structure:
337+
338+
```html
339+
<html>
340+
<body>
341+
<div>
342+
<div><!-- haystackElement -->
343+
<div>
344+
<div><!-- needleElement --></div>
345+
</div>
346+
</div>
347+
</div>
348+
</body>
349+
</html>
350+
```
351+
352+
If you generate the selector **without** the `useScope` option:
353+
354+
```javascript
355+
getCssSelector(needleElement, {
356+
root: haystackElement,
357+
useScope: false,
358+
});
359+
```
360+
361+
...it will produce this fallback selector:
362+
363+
```
364+
:root > :nth-child(1) > :nth-child(1) > :nth-child(1) > :nth-child(1) > :nth-child(1)
365+
```
366+
367+
... where the selectors correspond with these elements:
368+
369+
```
370+
:root -> <html>
371+
> :nth-child(1) -> <body>
372+
> :nth-child(1) -> <div>
373+
> :nth-child(1) -> <div> <!-- haystackElement -->
374+
> :nth-child(1) -> <div>
375+
> :nth-child(1) -> <div> <!-- needleElement -->
376+
```
377+
378+
But if you generate the selector **with** the `useScope` option:
379+
380+
```javascript
381+
getCssSelector(needleElement, {
382+
root: haystackElement,
383+
useScope: true,
384+
});
385+
```
386+
387+
...it will produce this fallback selector:
388+
389+
```:scope > :nth-child(1) > :nth-child(1)```
390+
391+
... where the selectors correspond with these elements:
392+
393+
```
394+
:scope -> <div> <!-- haystackElement -->
395+
> :nth-child(1) -> <div>
396+
> :nth-child(1) -> <div> <!-- needleElement -->
397+
```
398+
329399
## Bug reports, feature requests and contact
330400
331401
If you found any bugs, if you have feature requests or any questions, please, either [file an issue on GitHub][1] or send me an e-mail at [riki@fczbkk.com][2]

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function getCssSelector(
5252
.join(SELECTOR_SEPARATOR);
5353
}
5454

55-
return getFallbackSelector(elements);
55+
return getFallbackSelector(elements, options.useScope ? root : undefined);
5656
}
5757

5858
export default getCssSelector;

src/selector-fallback.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import {
99
/**
1010
* Creates fallback selector for single element.
1111
*/
12-
export function getElementFallbackSelector(element: Element): CssSelector {
13-
const parentElements = getElementParents(element).reverse();
12+
export function getElementFallbackSelector(
13+
element: Element,
14+
root?: ParentNode,
15+
): CssSelector {
16+
const parentElements = getElementParents(element, root).reverse();
1417
const elementsData = parentElements.map((element) => {
1518
const elementData = createElementData(
1619
element,
@@ -23,12 +26,20 @@ export function getElementFallbackSelector(element: Element): CssSelector {
2326
return elementData;
2427
});
2528

26-
return [":root", ...elementsData.map(constructElementSelector)].join("");
29+
return [
30+
root ? ":scope" : ":root",
31+
...elementsData.map(constructElementSelector),
32+
].join("");
2733
}
2834

2935
/**
3036
* Creates chain of :nth-child selectors from root to the elements.
3137
*/
32-
export function getFallbackSelector(elements: Element[]): CssSelector {
33-
return elements.map(getElementFallbackSelector).join(SELECTOR_SEPARATOR);
38+
export function getFallbackSelector(
39+
elements: Element[],
40+
root?: ParentNode,
41+
): CssSelector {
42+
return elements
43+
.map((element) => getElementFallbackSelector(element, root))
44+
.join(SELECTOR_SEPARATOR);
3445
}

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export type CssSelectorGeneratorOptionsInput = Partial<{
7373
maxCombinations: number;
7474
// Maximum number of selector candidates to be tested for each element. This is handy for performance reasons, e.g. when elements can produce large number of combinations of various types of selectors.
7575
maxCandidates: number;
76+
// Experimental. If set to `true` and the "root" option is set, the fallback selectors will use ":scope" pseudo-class to make the selectors shorter and simpler.
77+
useScope: boolean;
7678
}>;
7779

7880
export type CssSelectorGeneratorOptions = Required<

src/utilities-options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const DEFAULT_OPTIONS = {
2424
root: null,
2525
maxCombinations: Number.POSITIVE_INFINITY,
2626
maxCandidates: Number.POSITIVE_INFINITY,
27+
useScope: false,
2728
} as CssSelectorGeneratorOptions;
2829

2930
/**
@@ -142,5 +143,6 @@ export function sanitizeOptions(
142143
includeTag: !!options.includeTag,
143144
maxCombinations: sanitizeMaxNumber(options.maxCombinations),
144145
maxCandidates: sanitizeMaxNumber(options.maxCandidates),
146+
useScope: !!options.useScope,
145147
};
146148
}

test/selector-fallback.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,19 @@ describe("selector - fallback", function () {
6666
assert.ok(testMultiSelector(element, result, root));
6767
});
6868
});
69+
70+
it("should use :scope when the root option is set", () => {
71+
root.innerHTML = `
72+
<div>root
73+
<div>
74+
<div>needle</div>
75+
</div>
76+
</div>
77+
`;
78+
const rootElement = root.firstElementChild;
79+
const needleElement = rootElement.firstElementChild.firstElementChild;
80+
const result = getFallbackSelector([needleElement], rootElement);
81+
assert.ok(testSelector([needleElement], result, rootElement));
82+
assert.equal(result, ":scope > :nth-child(1) > :nth-child(1)");
83+
});
6984
});

0 commit comments

Comments
 (0)