Skip to content

Commit

Permalink
feat: Accept single filter function rather than dict
Browse files Browse the repository at this point in the history
  • Loading branch information
mike-plummer committed Apr 29, 2024
1 parent 74c9036 commit ace7c45
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 49 deletions.
5 changes: 3 additions & 2 deletions src/getAttribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
* Returns the {attr} selector of the element
* @param { Element } el - The element.
* @param { String } attribute - The attribute name.
* @param { Function } filter
* @return { String | null } - The {attr} selector of the element.
*/
export const getAttributeSelector = ( el, attribute ) =>
export const getAttributeSelector = ( el, attribute, filter ) =>
{
const attributeValue = el.getAttribute(attribute)

if (attributeValue === null) {
if (attributeValue === null || (filter && !filter('attribute', attribute, attributeValue))) {
return null
}

Expand Down
2 changes: 1 addition & 1 deletion src/getAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function getAttributes( el, attributesToIgnore = ['id', 'class', 'length'

return attrs.reduce( ( sum, next ) =>
{
if ( ! ( attributesToIgnore.indexOf( next.nodeName ) > -1 ) && (!filter || filter('attributes', next.nodeName, next.value)) )
if ( ! ( attributesToIgnore.indexOf( next.nodeName ) > -1 ) && (!filter || filter('attribute', next.nodeName, next.value)) )
{
sum.push( `[${next.nodeName}="${next.value}"]` );
}
Expand Down
2 changes: 1 addition & 1 deletion src/getClasses.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function getClasses( el, filter )

try {
return Array.prototype.slice.call( el.classList )
.filter((cls) => !filter || filter('class', 'class', cls));
.filter((cls) => !filter || filter('attribute', 'class', cls));
} catch (e) {
let className = el.getAttribute( 'class' );

Expand Down
2 changes: 1 addition & 1 deletion src/getID.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function getID( el, filter )
{
const id = el.getAttribute( 'id' );

if( id !== null && id !== '' && (!filter || filter('id', 'id', id)))
if( id !== null && id !== '' && (!filter || filter('attribute', 'id', id)))
{
return `#${CSS.escape( id )}`;
}
Expand Down
2 changes: 1 addition & 1 deletion src/getName.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function getName( el, filter )
{
const name = el.getAttribute( 'name' );

if( name !== null && name !== '' && (!filter || filter('name', 'name', name)))
if( name !== null && name !== '' && (!filter || filter('attribute', 'name', name)))
{
return `[name="${name}"]`;
}
Expand Down
52 changes: 32 additions & 20 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ import { getAttributeSelector } from './getAttribute';
const dataRegex = /^data-.+/;
const attrRegex = /^attribute:(.+)/m;

/**
* @typedef Filter
* @type {Function}
* @param {string} type - the trait being considered ('attribute', 'tag', 'nth-child').
* @param {string} key - your trait key (for 'attribute' will be the attribute name, for others will typically be the same as 'type').
* @param {string} value - the trait value.
* @returns {boolean} whether this trait can be used when building the selector (true = allow). Defaults to 'true' if no value returned.
*/

/**
* Returns all the selectors of the element
* @param { Object } element
* @return { Object }
*/
function getAllSelectors( el, selectors, attributesToIgnore, filters )
function getAllSelectors( el, selectors, attributesToIgnore, filter )
{
const consolidatedAttributesToIgnore = [...attributesToIgnore]
const nonAttributeSelectors = []
Expand All @@ -37,12 +46,12 @@ function getAllSelectors( el, selectors, attributesToIgnore, filters )

const funcs =
{
'tag' : elem => getTag( elem, filters.tag ),
'nth-child' : elem => getNthChild( elem, filters.nthChild ),
'attributes' : elem => getAttributes( elem, consolidatedAttributesToIgnore, filters.attributes ),
'class' : elem => getClassSelectors( elem, filters.class ),
'id' : elem => getID( elem, filters.id ),
'name' : elem => getName (elem, filters.name ),
'tag' : elem => getTag( elem, filter ),
'nth-child' : elem => getNthChild( elem, filter ),
'attributes' : elem => getAttributes( elem, consolidatedAttributesToIgnore, filter ),
'class' : elem => getClassSelectors( elem, filter ),
'id' : elem => getID( elem, filter ),
'name' : elem => getName (elem, filter ),
};

return nonAttributeSelectors
Expand Down Expand Up @@ -118,11 +127,11 @@ function getUniqueCombination( element, items, tag )
* @param { Array } options
* @return { String }
*/
function getUniqueSelector( element, selectorTypes, attributesToIgnore, filters )
function getUniqueSelector( element, selectorTypes, attributesToIgnore, filter )
{
let foundSelector;

const elementSelectors = getAllSelectors( element, selectorTypes, attributesToIgnore, filters );
const elementSelectors = getAllSelectors( element, selectorTypes, attributesToIgnore, filter );

for( let selectorType of selectorTypes )
{
Expand All @@ -134,13 +143,11 @@ function getUniqueSelector( element, selectorTypes, attributesToIgnore, filters
if ( isDataAttributeSelectorType || isAttributeSelectorType )
{
const attributeToQuery = isDataAttributeSelectorType ? selectorType : selectorType.replace(attrRegex, '$1')
const attributeValue = element.getAttribute(attributeToQuery)
const attributeFilter = filters[selectorType];

const attributeSelector = getAttributeSelector(element, attributeToQuery, filter)
// if we found a selector via attribute
if ( attributeValue !== null && (!attributeFilter || attributeFilter(selectorType, attributeToQuery, attributeValue)) )
if ( attributeSelector )
{
selector = getAttributeSelector( element, attributeToQuery );
selector = getAttributeSelector( element, attributeToQuery, filter );
selectorType = 'attribute';
}
}
Expand Down Expand Up @@ -187,10 +194,7 @@ function getUniqueSelector( element, selectorTypes, attributesToIgnore, filters
* @param {Object} options (optional) Customize various behaviors of selector generation
* @param {String[]} options.selectorTypes Specify the set of traits to leverage when building selectors in precedence order
* @param {String[]} options.attributesToIgnore Specify a set of attributes to *not* leverage when building selectors
* @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:
* * selectorType: The selector type/category being generated
* * key: The key being evaluated - this will typically match the `selectorType` except in aggregate types like `attributes`
* * value: The value to consider. Returning `true` will allow its use in selector generation, `false` will prevent.
* @param {Filter} options.filter Provide a filter function to conditionally reject various traits when building selectors.
* @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.
* @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.
* @return {String}
Expand All @@ -201,10 +205,18 @@ export default function unique( el, options={} ) {
const {
selectorTypes=['id', 'name', 'class', 'tag', 'nth-child'],
attributesToIgnore= ['id', 'class', 'length'],
filters = {},
filter,
selectorCache,
isUniqueCache
} = options;
// If filter was provided wrap it to ensure a default value of `true` is returned if the provided function fails to return a value
const normalizedFilter = filter && function(type, key, value) {
const result = filter(type, key, value)
if (result === null || result === undefined) {
return true
}
return result
}
const allSelectors = [];

let currentElement = el
Expand All @@ -216,7 +228,7 @@ export default function unique( el, options={} ) {
currentElement,
selectorTypes,
attributesToIgnore,
filters
normalizedFilter
)
if (selectorCache) {
selectorCache.set(currentElement, selector)
Expand Down
76 changes: 53 additions & 23 deletions test/unique-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,20 @@ describe( 'Unique Selector Tests', () =>
} );

it('ID filters appropriately', () => {
const filters = {
'id': (type, key, value) => {
const filter = (type, key, value) => {
if (type === 'attribute' && key === 'id') {
return /oo/.test(value)
}
return true
}
let el = $.parseHTML( '<div id="foo"></div>' )[0];
$(el).appendTo('body')
let uniqueSelector = unique( el, { filters } );
let uniqueSelector = unique( el, { filter } );
expect( uniqueSelector ).to.equal( '#foo' );

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

Expand Down Expand Up @@ -84,19 +85,20 @@ describe( 'Unique Selector Tests', () =>
} );

it('Classes filters appropriately', () => {
const filters = {
'class': (type, key, value) => {
const filter = (type, key, value) => {
if (type === 'attribute' && key === 'class') {
return value.startsWith('a')
}
return true
}
let el = $.parseHTML( '<div class="a1"></div>' )[0];
$(el).appendTo('body')
let uniqueSelector = unique( el, { filters } );
let uniqueSelector = unique( el, { filter } );
expect( uniqueSelector ).to.equal( '.a1' );

el = $.parseHTML( '<div class="b1 a2"></div>' )[0];
$(el).appendTo('body')
uniqueSelector = unique( el, { filters } );
uniqueSelector = unique( el, { filter } );
expect( uniqueSelector ).to.equal( '.a2' );
});

Expand Down Expand Up @@ -141,9 +143,11 @@ describe( 'Unique Selector Tests', () =>
// by other selectorType generators
const uniqueSelector = unique( el, {
selectorTypes : ['data-foo', 'attribute:a', 'attributes', 'nth-child'],
filters: {
'data-foo': () => false,
'attribute:a': () => false,
filter: (type, key, value) => {
if (type === 'attribute' && ['data-foo', 'a'].includes(key)) {
return false
}
return true
}
} );
expect( uniqueSelector ).to.equal( ':nth-child(2) > :nth-child(1)' );
Expand Down Expand Up @@ -183,19 +187,20 @@ describe( 'Unique Selector Tests', () =>
} );

it('filters appropriately', () => {
const filters = {
'data-foo': (type, key, value) => {
const filter = (type, key, value) => {
if (type === 'attribute' && key === 'data-foo') {
return value === 'abc'
}
return true
}
let el = $.parseHTML( '<div data-foo="abc" class="test1"></div>' )[0];
$(el).appendTo('body')
let uniqueSelector = unique( el, { filters, selectorTypes : ['data-foo', 'class'] } );
let uniqueSelector = unique( el, { filter, selectorTypes : ['data-foo', 'class'] } );
expect( uniqueSelector ).to.equal( '[data-foo="abc"]' );

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

it('filters appropriately', () => {
const filters = {
'attribute:role': (type, key, value) => {
const filter = (type, key, value) => {
if (type === 'attribute' && key === 'role') {
return value === 'abc'
}
return true
}
let el = $.parseHTML( '<div role="abc" class="test1"></div>' )[0];
$(el).appendTo('body')
let uniqueSelector = unique( el, { filters, selectorTypes : ['attribute:role', 'class'] } );
let uniqueSelector = unique( el, { filter, selectorTypes : ['attribute:role', 'class'] } );
expect( uniqueSelector ).to.equal( '[role="abc"]' );

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

it('filters appropriately', () => {
const filters = {
'name': (type, key, value) => {
const filter = (type, key, value) => {
if (type === 'attribute' && key === 'name') {
return value === 'abc'
}
return true
}
let el = $.parseHTML( '<div name="abc" class="test1"></div>' )[0];
$(el).appendTo('body')
let uniqueSelector = unique( el, { filters } );
let uniqueSelector = unique( el, { filter } );
expect( uniqueSelector ).to.equal( '[name="abc"]' );

el = $.parseHTML( '<div name="def" class="test2"></div>' )[0];
$(el).appendTo('body')
uniqueSelector = unique( el, { filters } );
uniqueSelector = unique( el, { filter } );
expect( uniqueSelector ).to.equal( '.test2' );
})
})

describe('nth-child', () => {
it( 'builds expected selector', () =>
{
$( 'body' ).append( '<div><div class="test-nth-child"></div></div>' );
const findNode = $( 'body' ).find( '.test-nth-child' ).get( 0 );
const uniqueSelector = unique( findNode, { selectorTypes : ['nth-child'] } );
expect( uniqueSelector ).to.equal( ':nth-child(2) > :nth-child(1) > :nth-child(1)' );
} );

it('filters appropriately', () => {
const filter = (type, key, value) => {
if (type === 'nth-child') {
return value !== 1
}
return true
}
$( 'body' ).append( '<div><span class="test-nth-child"></span></div>' )[0];
const findNode = $( 'body' ).find( '.test-nth-child' ).get( 0 );
const uniqueSelector = unique( findNode, { filter, selectorTypes : ['nth-child', 'tag'] } );
expect( uniqueSelector ).to.equal( 'span' );
})
})
} );

0 comments on commit ace7c45

Please sign in to comment.