Skip to content

Commit 74c9036

Browse files
authored
feat: Allow selector traits to be conditionally filtered (#8)
1 parent 993a2b9 commit 74c9036

9 files changed

+188
-78
lines changed

src/getAttribute.js

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,21 @@
11
/**
22
* Returns the {attr} selector of the element
3-
* @param { String } selectorType - The attribute selector to return.
4-
* @param { String } attributes - The attributes of the element.
3+
* @param { Element } el - The element.
4+
* @param { String } attribute - The attribute name.
55
* @return { String | null } - The {attr} selector of the element.
66
*/
7-
8-
export const getAttribute = ( selectorType, attributes ) =>
7+
export const getAttributeSelector = ( el, attribute ) =>
98
{
10-
for ( let i = 0; i < attributes.length; i++ )
11-
{
12-
// extract node name + value
13-
const { nodeName, value } = attributes[ i ];
9+
const attributeValue = el.getAttribute(attribute)
1410

15-
// if this matches our selector
16-
if ( nodeName === selectorType )
17-
{
18-
if ( value )
19-
{
20-
// if we have value that needs quotes
21-
return `[${nodeName}="${value}"]`;
22-
}
11+
if (attributeValue === null) {
12+
return null
13+
}
2314

24-
return `[${nodeName}]`;
25-
}
15+
if (attributeValue) {
16+
// if we have value that needs quotes
17+
return `[${attribute}="${attributeValue}"]`;
2618
}
2719

28-
return null;
20+
return `[${attribute}]`;
2921
};

src/getAttributes.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
/**
22
* Returns the Attribute selectors of the element
3-
* @param { DOM Element } element
3+
* @param { Element } element
44
* @param { Array } array of attributes to ignore
5+
* @param { Function } filter
56
* @return { Array }
67
*/
7-
export function getAttributes( el, attributesToIgnore = ['id', 'class', 'length'] )
8+
export function getAttributes( el, attributesToIgnore = ['id', 'class', 'length'], filter )
89
{
910
const { attributes } = el;
1011
const attrs = [ ...attributes ];
1112

1213
return attrs.reduce( ( sum, next ) =>
1314
{
14-
if ( ! ( attributesToIgnore.indexOf( next.nodeName ) > -1 ) )
15+
if ( ! ( attributesToIgnore.indexOf( next.nodeName ) > -1 ) && (!filter || filter('attributes', next.nodeName, next.value)) )
1516
{
1617
sum.push( `[${next.nodeName}="${next.value}"]` );
1718
}

src/getClasses.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,40 @@ import 'css.escape';
33
/**
44
* Get class names for an element
55
*
6-
* @pararm { Element } el
6+
* @param { Element } el
7+
* @param { Function } filter
78
* @return { Array }
89
*/
9-
export function getClasses( el )
10+
export function getClasses( el, filter )
1011
{
1112
if( !el.hasAttribute( 'class' ) )
1213
{
1314
return [];
1415
}
1516

1617
try {
17-
return Array.prototype.slice.call( el.classList );
18+
return Array.prototype.slice.call( el.classList )
19+
.filter((cls) => !filter || filter('class', 'class', cls));
1820
} catch (e) {
1921
let className = el.getAttribute( 'class' );
2022

2123
// remove duplicate and leading/trailing whitespaces
22-
className = className.trim().replace( /\s+/g, ' ' );
24+
className = className.trim()
2325

24-
// split into separate classnames
25-
return className.split( ' ' );
26+
// split into separate classnames, perform filtering
27+
return className.split(/\s+/g)
28+
.filter((cls) => !filter || filter('class', 'class', cls));
2629
}
2730
}
2831

2932
/**
3033
* Returns the Class selectors of the element
3134
* @param { Object } element
35+
* @param { Function } filter
3236
* @return { Array }
3337
*/
34-
export function getClassSelectors( el )
38+
export function getClassSelectors( el, filter )
3539
{
36-
const classList = getClasses( el ).filter( Boolean );
40+
const classList = getClasses( el, filter ).filter( Boolean );
3741
return classList.map( cl => `.${CSS.escape( cl )}` );
3842
}

src/getID.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import 'css.escape';
33
/**
44
* Returns the Tag of the element
55
* @param { Object } element
6+
* @param { Function } filter
67
* @return { String }
78
*/
8-
export function getID( el )
9+
export function getID( el, filter )
910
{
1011
const id = el.getAttribute( 'id' );
1112

12-
if( id !== null && id !== '')
13+
if( id !== null && id !== '' && (!filter || filter('id', 'id', id)))
1314
{
1415
return `#${CSS.escape( id )}`;
1516
}

src/getName.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/**
22
* Returns the `name` attribute of the element (if one exists)
33
* @param { Object } element
4+
* @param { Function } filter
45
* @return { String }
56
*/
6-
export function getName( el )
7+
export function getName( el, filter )
78
{
89
const name = el.getAttribute( 'name' );
910

10-
if( name !== null && name !== '')
11+
if( name !== null && name !== '' && (!filter || filter('name', 'name', name)))
1112
{
1213
return `[name="${name}"]`;
1314
}

src/getNthChild.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { isElement } from './isElement';
33
/**
44
* Returns the selectors based on the position of the element relative to its siblings
55
* @param { Object } element
6+
* @param { Function } filter
67
* @return { Array }
78
*/
8-
export function getNthChild( element )
9+
export function getNthChild( element, filter )
910
{
1011
let counter = 0;
1112
let k;
@@ -22,7 +23,7 @@ export function getNthChild( element )
2223
if( isElement( sibling ) )
2324
{
2425
counter++;
25-
if( sibling === element )
26+
if( sibling === element && (!filter || filter('nth-child', 'nth-child', counter)) )
2627
{
2728
return `:nth-child(${counter})`;
2829
}

src/getTag.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
/**
22
* Returns the Tag of the element
33
* @param { Object } element
4+
* @param { Function } filter
45
* @return { String }
56
*/
6-
export function getTag( el )
7+
export function getTag( el, filter )
78
{
8-
return el.tagName.toLowerCase().replace(/:/g, '\\:');
9+
const tagName = el.tagName.toLowerCase().replace(/:/g, '\\:')
10+
11+
if (filter && !filter('tag', 'tag', tagName)) {
12+
return null;
13+
}
14+
15+
return tagName;
916
}

src/index.js

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getNthChild } from './getNthChild';
1111
import { getTag } from './getTag';
1212
import { isUnique } from './isUnique';
1313
import { getParents } from './getParents';
14-
import { getAttribute } from './getAttribute';
14+
import { getAttributeSelector } from './getAttribute';
1515

1616
const dataRegex = /^data-.+/;
1717
const attrRegex = /^attribute:(.+)/m;
@@ -21,20 +21,31 @@ const attrRegex = /^attribute:(.+)/m;
2121
* @param { Object } element
2222
* @return { Object }
2323
*/
24-
function getAllSelectors( el, selectors, attributesToIgnore )
24+
function getAllSelectors( el, selectors, attributesToIgnore, filters )
2525
{
26+
const consolidatedAttributesToIgnore = [...attributesToIgnore]
27+
const nonAttributeSelectors = []
28+
for (const selectorType of selectors) {
29+
if (dataRegex.test(selectorType)) {
30+
consolidatedAttributesToIgnore.push(selectorType)
31+
} else if (attrRegex.test(selectorType)) {
32+
consolidatedAttributesToIgnore.push(selectorType.replace(attrRegex, '$1'))
33+
} else {
34+
nonAttributeSelectors.push(selectorType)
35+
}
36+
}
37+
2638
const funcs =
2739
{
28-
'tag' : getTag,
29-
'nth-child' : getNthChild,
30-
'attributes' : elem => getAttributes( elem, attributesToIgnore ),
31-
'class' : getClassSelectors,
32-
'id' : getID,
33-
'name' : getName,
40+
'tag' : elem => getTag( elem, filters.tag ),
41+
'nth-child' : elem => getNthChild( elem, filters.nthChild ),
42+
'attributes' : elem => getAttributes( elem, consolidatedAttributesToIgnore, filters.attributes ),
43+
'class' : elem => getClassSelectors( elem, filters.class ),
44+
'id' : elem => getID( elem, filters.id ),
45+
'name' : elem => getName (elem, filters.name ),
3446
};
3547

36-
return selectors
37-
.filter( ( selector ) => !dataRegex.test( selector ) && !attrRegex.test( selector ) )
48+
return nonAttributeSelectors
3849
.reduce( ( res, next ) =>
3950
{
4051
res[ next ] = funcs[ next ]( el );
@@ -107,13 +118,11 @@ function getUniqueCombination( element, items, tag )
107118
* @param { Array } options
108119
* @return { String }
109120
*/
110-
function getUniqueSelector( element, selectorTypes, attributesToIgnore )
121+
function getUniqueSelector( element, selectorTypes, attributesToIgnore, filters )
111122
{
112123
let foundSelector;
113124

114-
const attributes = [...element.attributes];
115-
116-
const elementSelectors = getAllSelectors( element, selectorTypes, attributesToIgnore );
125+
const elementSelectors = getAllSelectors( element, selectorTypes, attributesToIgnore, filters );
117126

118127
for( let selectorType of selectorTypes )
119128
{
@@ -125,12 +134,13 @@ function getUniqueSelector( element, selectorTypes, attributesToIgnore )
125134
if ( isDataAttributeSelectorType || isAttributeSelectorType )
126135
{
127136
const attributeToQuery = isDataAttributeSelectorType ? selectorType : selectorType.replace(attrRegex, '$1')
128-
const attributeSelector = getAttribute( attributeToQuery, attributes );
137+
const attributeValue = element.getAttribute(attributeToQuery)
138+
const attributeFilter = filters[selectorType];
129139

130140
// if we found a selector via attribute
131-
if ( attributeSelector )
141+
if ( attributeValue !== null && (!attributeFilter || attributeFilter(selectorType, attributeToQuery, attributeValue)) )
132142
{
133-
selector = attributeSelector;
143+
selector = getAttributeSelector( element, attributeToQuery );
134144
selectorType = 'attribute';
135145
}
136146
}
@@ -174,6 +184,15 @@ function getUniqueSelector( element, selectorTypes, attributesToIgnore )
174184
* Generate unique CSS selector for given DOM element
175185
*
176186
* @param {Element} el
187+
* @param {Object} options (optional) Customize various behaviors of selector generation
188+
* @param {String[]} options.selectorTypes Specify the set of traits to leverage when building selectors in precedence order
189+
* @param {String[]} options.attributesToIgnore Specify a set of attributes to *not* leverage when building selectors
190+
* @param {Object} options.filters Specify a set of filter functions to conditionally reject various traits when building selectors. Keys correspond to a `selectorTypes` entry, values should be a function accepting three parameters:
191+
* * selectorType: The selector type/category being generated
192+
* * key: The key being evaluated - this will typically match the `selectorType` except in aggregate types like `attributes`
193+
* * value: The value to consider. Returning `true` will allow its use in selector generation, `false` will prevent.
194+
* @param {Map<Element, String>} options.selectorCache Provide a cache to improve performance of repeated selector generation - it is the responsibility of the caller to handle cache invalidation. Caching is performed using the input Element as key. This cache handles Element -> Selector caching.
195+
* @param {Map<String, Boolean>} options.isUniqueCache Provide a cache to improve performance of repeated selector generation - it is the responsibility of the caller to handle cache invalidation. Caching is performed using the input Element as key. This cache handles Selector -> isUnique caching.
177196
* @return {String}
178197
* @api private
179198
*/
@@ -182,6 +201,7 @@ export default function unique( el, options={} ) {
182201
const {
183202
selectorTypes=['id', 'name', 'class', 'tag', 'nth-child'],
184203
attributesToIgnore= ['id', 'class', 'length'],
204+
filters = {},
185205
selectorCache,
186206
isUniqueCache
187207
} = options;
@@ -195,7 +215,8 @@ export default function unique( el, options={} ) {
195215
selector = getUniqueSelector(
196216
currentElement,
197217
selectorTypes,
198-
attributesToIgnore
218+
attributesToIgnore,
219+
filters
199220
)
200221
if (selectorCache) {
201222
selectorCache.set(currentElement, selector)

0 commit comments

Comments
 (0)