Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Accept single filter function rather than dict #9

Merged
merged 2 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = attributeSelector
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' );
})
})
} );
Loading