Skip to content

Commit 5b5afe4

Browse files
authored
feat: Accept single filter function rather than dict (#9)
BREAKING CHANGE: Optional filter argument is now a function rather than an object
1 parent 74c9036 commit 5b5afe4

File tree

7 files changed

+92
-49
lines changed

7 files changed

+92
-49
lines changed

src/getAttribute.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
* Returns the {attr} selector of the element
33
* @param { Element } el - The element.
44
* @param { String } attribute - The attribute name.
5+
* @param { Function } filter
56
* @return { String | null } - The {attr} selector of the element.
67
*/
7-
export const getAttributeSelector = ( el, attribute ) =>
8+
export const getAttributeSelector = ( el, attribute, filter ) =>
89
{
910
const attributeValue = el.getAttribute(attribute)
1011

11-
if (attributeValue === null) {
12+
if (attributeValue === null || (filter && !filter('attribute', attribute, attributeValue))) {
1213
return null
1314
}
1415

src/getAttributes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function getAttributes( el, attributesToIgnore = ['id', 'class', 'length'
1212

1313
return attrs.reduce( ( sum, next ) =>
1414
{
15-
if ( ! ( attributesToIgnore.indexOf( next.nodeName ) > -1 ) && (!filter || filter('attributes', next.nodeName, next.value)) )
15+
if ( ! ( attributesToIgnore.indexOf( next.nodeName ) > -1 ) && (!filter || filter('attribute', next.nodeName, next.value)) )
1616
{
1717
sum.push( `[${next.nodeName}="${next.value}"]` );
1818
}

src/getClasses.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function getClasses( el, filter )
1616

1717
try {
1818
return Array.prototype.slice.call( el.classList )
19-
.filter((cls) => !filter || filter('class', 'class', cls));
19+
.filter((cls) => !filter || filter('attribute', 'class', cls));
2020
} catch (e) {
2121
let className = el.getAttribute( 'class' );
2222

src/getID.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function getID( el, filter )
1010
{
1111
const id = el.getAttribute( 'id' );
1212

13-
if( id !== null && id !== '' && (!filter || filter('id', 'id', id)))
13+
if( id !== null && id !== '' && (!filter || filter('attribute', 'id', id)))
1414
{
1515
return `#${CSS.escape( id )}`;
1616
}

src/getName.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function getName( el, filter )
88
{
99
const name = el.getAttribute( 'name' );
1010

11-
if( name !== null && name !== '' && (!filter || filter('name', 'name', name)))
11+
if( name !== null && name !== '' && (!filter || filter('attribute', 'name', name)))
1212
{
1313
return `[name="${name}"]`;
1414
}

src/index.js

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,21 @@ import { getAttributeSelector } from './getAttribute';
1616
const dataRegex = /^data-.+/;
1717
const attrRegex = /^attribute:(.+)/m;
1818

19+
/**
20+
* @typedef Filter
21+
* @type {Function}
22+
* @param {string} type - the trait being considered ('attribute', 'tag', 'nth-child').
23+
* @param {string} key - your trait key (for 'attribute' will be the attribute name, for others will typically be the same as 'type').
24+
* @param {string} value - the trait value.
25+
* @returns {boolean} whether this trait can be used when building the selector (true = allow). Defaults to 'true' if no value returned.
26+
*/
27+
1928
/**
2029
* Returns all the selectors of the element
2130
* @param { Object } element
2231
* @return { Object }
2332
*/
24-
function getAllSelectors( el, selectors, attributesToIgnore, filters )
33+
function getAllSelectors( el, selectors, attributesToIgnore, filter )
2534
{
2635
const consolidatedAttributesToIgnore = [...attributesToIgnore]
2736
const nonAttributeSelectors = []
@@ -37,12 +46,12 @@ function getAllSelectors( el, selectors, attributesToIgnore, filters )
3746

3847
const funcs =
3948
{
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 ),
49+
'tag' : elem => getTag( elem, filter ),
50+
'nth-child' : elem => getNthChild( elem, filter ),
51+
'attributes' : elem => getAttributes( elem, consolidatedAttributesToIgnore, filter ),
52+
'class' : elem => getClassSelectors( elem, filter ),
53+
'id' : elem => getID( elem, filter ),
54+
'name' : elem => getName (elem, filter ),
4655
};
4756

4857
return nonAttributeSelectors
@@ -118,11 +127,11 @@ function getUniqueCombination( element, items, tag )
118127
* @param { Array } options
119128
* @return { String }
120129
*/
121-
function getUniqueSelector( element, selectorTypes, attributesToIgnore, filters )
130+
function getUniqueSelector( element, selectorTypes, attributesToIgnore, filter )
122131
{
123132
let foundSelector;
124133

125-
const elementSelectors = getAllSelectors( element, selectorTypes, attributesToIgnore, filters );
134+
const elementSelectors = getAllSelectors( element, selectorTypes, attributesToIgnore, filter );
126135

127136
for( let selectorType of selectorTypes )
128137
{
@@ -134,13 +143,11 @@ function getUniqueSelector( element, selectorTypes, attributesToIgnore, filters
134143
if ( isDataAttributeSelectorType || isAttributeSelectorType )
135144
{
136145
const attributeToQuery = isDataAttributeSelectorType ? selectorType : selectorType.replace(attrRegex, '$1')
137-
const attributeValue = element.getAttribute(attributeToQuery)
138-
const attributeFilter = filters[selectorType];
139-
146+
const attributeSelector = getAttributeSelector(element, attributeToQuery, filter)
140147
// if we found a selector via attribute
141-
if ( attributeValue !== null && (!attributeFilter || attributeFilter(selectorType, attributeToQuery, attributeValue)) )
148+
if ( attributeSelector )
142149
{
143-
selector = getAttributeSelector( element, attributeToQuery );
150+
selector = attributeSelector
144151
selectorType = 'attribute';
145152
}
146153
}
@@ -187,10 +194,7 @@ function getUniqueSelector( element, selectorTypes, attributesToIgnore, filters
187194
* @param {Object} options (optional) Customize various behaviors of selector generation
188195
* @param {String[]} options.selectorTypes Specify the set of traits to leverage when building selectors in precedence order
189196
* @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.
197+
* @param {Filter} options.filter Provide a filter function to conditionally reject various traits when building selectors.
194198
* @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.
195199
* @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.
196200
* @return {String}
@@ -201,10 +205,18 @@ export default function unique( el, options={} ) {
201205
const {
202206
selectorTypes=['id', 'name', 'class', 'tag', 'nth-child'],
203207
attributesToIgnore= ['id', 'class', 'length'],
204-
filters = {},
208+
filter,
205209
selectorCache,
206210
isUniqueCache
207211
} = options;
212+
// If filter was provided wrap it to ensure a default value of `true` is returned if the provided function fails to return a value
213+
const normalizedFilter = filter && function(type, key, value) {
214+
const result = filter(type, key, value)
215+
if (result === null || result === undefined) {
216+
return true
217+
}
218+
return result
219+
}
208220
const allSelectors = [];
209221

210222
let currentElement = el
@@ -216,7 +228,7 @@ export default function unique( el, options={} ) {
216228
currentElement,
217229
selectorTypes,
218230
attributesToIgnore,
219-
filters
231+
normalizedFilter
220232
)
221233
if (selectorCache) {
222234
selectorCache.set(currentElement, selector)

test/unique-selector.js

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,20 @@ describe( 'Unique Selector Tests', () =>
2727
} );
2828

2929
it('ID filters appropriately', () => {
30-
const filters = {
31-
'id': (type, key, value) => {
30+
const filter = (type, key, value) => {
31+
if (type === 'attribute' && key === 'id') {
3232
return /oo/.test(value)
3333
}
34+
return true
3435
}
3536
let el = $.parseHTML( '<div id="foo"></div>' )[0];
3637
$(el).appendTo('body')
37-
let uniqueSelector = unique( el, { filters } );
38+
let uniqueSelector = unique( el, { filter } );
3839
expect( uniqueSelector ).to.equal( '#foo' );
3940

4041
el = $.parseHTML( '<div id="bar"></div>' )[0];
4142
$(el).appendTo('body')
42-
uniqueSelector = unique( el, { filters } );
43+
uniqueSelector = unique( el, { filter } );
4344
expect( uniqueSelector ).to.equal( 'body > :nth-child(2)' );
4445
});
4546

@@ -84,19 +85,20 @@ describe( 'Unique Selector Tests', () =>
8485
} );
8586

8687
it('Classes filters appropriately', () => {
87-
const filters = {
88-
'class': (type, key, value) => {
88+
const filter = (type, key, value) => {
89+
if (type === 'attribute' && key === 'class') {
8990
return value.startsWith('a')
9091
}
92+
return true
9193
}
9294
let el = $.parseHTML( '<div class="a1"></div>' )[0];
9395
$(el).appendTo('body')
94-
let uniqueSelector = unique( el, { filters } );
96+
let uniqueSelector = unique( el, { filter } );
9597
expect( uniqueSelector ).to.equal( '.a1' );
9698

9799
el = $.parseHTML( '<div class="b1 a2"></div>' )[0];
98100
$(el).appendTo('body')
99-
uniqueSelector = unique( el, { filters } );
101+
uniqueSelector = unique( el, { filter } );
100102
expect( uniqueSelector ).to.equal( '.a2' );
101103
});
102104

@@ -141,9 +143,11 @@ describe( 'Unique Selector Tests', () =>
141143
// by other selectorType generators
142144
const uniqueSelector = unique( el, {
143145
selectorTypes : ['data-foo', 'attribute:a', 'attributes', 'nth-child'],
144-
filters: {
145-
'data-foo': () => false,
146-
'attribute:a': () => false,
146+
filter: (type, key, value) => {
147+
if (type === 'attribute' && ['data-foo', 'a'].includes(key)) {
148+
return false
149+
}
150+
return true
147151
}
148152
} );
149153
expect( uniqueSelector ).to.equal( ':nth-child(2) > :nth-child(1)' );
@@ -183,19 +187,20 @@ describe( 'Unique Selector Tests', () =>
183187
} );
184188

185189
it('filters appropriately', () => {
186-
const filters = {
187-
'data-foo': (type, key, value) => {
190+
const filter = (type, key, value) => {
191+
if (type === 'attribute' && key === 'data-foo') {
188192
return value === 'abc'
189193
}
194+
return true
190195
}
191196
let el = $.parseHTML( '<div data-foo="abc" class="test1"></div>' )[0];
192197
$(el).appendTo('body')
193-
let uniqueSelector = unique( el, { filters, selectorTypes : ['data-foo', 'class'] } );
198+
let uniqueSelector = unique( el, { filter, selectorTypes : ['data-foo', 'class'] } );
194199
expect( uniqueSelector ).to.equal( '[data-foo="abc"]' );
195200

196201
el = $.parseHTML( '<div data-foo="def" class="test2"></div>' )[0];
197202
$(el).appendTo('body')
198-
uniqueSelector = unique( el, { filters, selectorTypes : ['data-foo', 'class'] } );
203+
uniqueSelector = unique( el, { filter, selectorTypes : ['data-foo', 'class'] } );
199204
expect( uniqueSelector ).to.equal( '.test2' );
200205
})
201206
});
@@ -216,19 +221,20 @@ describe( 'Unique Selector Tests', () =>
216221
})
217222

218223
it('filters appropriately', () => {
219-
const filters = {
220-
'attribute:role': (type, key, value) => {
224+
const filter = (type, key, value) => {
225+
if (type === 'attribute' && key === 'role') {
221226
return value === 'abc'
222227
}
228+
return true
223229
}
224230
let el = $.parseHTML( '<div role="abc" class="test1"></div>' )[0];
225231
$(el).appendTo('body')
226-
let uniqueSelector = unique( el, { filters, selectorTypes : ['attribute:role', 'class'] } );
232+
let uniqueSelector = unique( el, { filter, selectorTypes : ['attribute:role', 'class'] } );
227233
expect( uniqueSelector ).to.equal( '[role="abc"]' );
228234

229235
el = $.parseHTML( '<div role="def" class="test2"></div>' )[0];
230236
$(el).appendTo('body')
231-
uniqueSelector = unique( el, { filters, selectorTypes : ['attribute:role', 'class'] } );
237+
uniqueSelector = unique( el, { filter, selectorTypes : ['attribute:role', 'class'] } );
232238
expect( uniqueSelector ).to.equal( '.test2' );
233239
})
234240
})
@@ -251,20 +257,44 @@ describe( 'Unique Selector Tests', () =>
251257
} );
252258

253259
it('filters appropriately', () => {
254-
const filters = {
255-
'name': (type, key, value) => {
260+
const filter = (type, key, value) => {
261+
if (type === 'attribute' && key === 'name') {
256262
return value === 'abc'
257263
}
264+
return true
258265
}
259266
let el = $.parseHTML( '<div name="abc" class="test1"></div>' )[0];
260267
$(el).appendTo('body')
261-
let uniqueSelector = unique( el, { filters } );
268+
let uniqueSelector = unique( el, { filter } );
262269
expect( uniqueSelector ).to.equal( '[name="abc"]' );
263270

264271
el = $.parseHTML( '<div name="def" class="test2"></div>' )[0];
265272
$(el).appendTo('body')
266-
uniqueSelector = unique( el, { filters } );
273+
uniqueSelector = unique( el, { filter } );
267274
expect( uniqueSelector ).to.equal( '.test2' );
268275
})
269276
})
277+
278+
describe('nth-child', () => {
279+
it( 'builds expected selector', () =>
280+
{
281+
$( 'body' ).append( '<div><div class="test-nth-child"></div></div>' );
282+
const findNode = $( 'body' ).find( '.test-nth-child' ).get( 0 );
283+
const uniqueSelector = unique( findNode, { selectorTypes : ['nth-child'] } );
284+
expect( uniqueSelector ).to.equal( ':nth-child(2) > :nth-child(1) > :nth-child(1)' );
285+
} );
286+
287+
it('filters appropriately', () => {
288+
const filter = (type, key, value) => {
289+
if (type === 'nth-child') {
290+
return value !== 1
291+
}
292+
return true
293+
}
294+
$( 'body' ).append( '<div><span class="test-nth-child"></span></div>' )[0];
295+
const findNode = $( 'body' ).find( '.test-nth-child' ).get( 0 );
296+
const uniqueSelector = unique( findNode, { filter, selectorTypes : ['nth-child', 'tag'] } );
297+
expect( uniqueSelector ).to.equal( 'span' );
298+
})
299+
})
270300
} );

0 commit comments

Comments
 (0)