diff --git a/src/getAttribute.js b/src/getAttribute.js index 68d407e..3833261 100644 --- a/src/getAttribute.js +++ b/src/getAttribute.js @@ -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 } diff --git a/src/getAttributes.js b/src/getAttributes.js index a177fa5..789d812 100644 --- a/src/getAttributes.js +++ b/src/getAttributes.js @@ -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}"]` ); } diff --git a/src/getClasses.js b/src/getClasses.js index f4ab76c..b2bdb4d 100644 --- a/src/getClasses.js +++ b/src/getClasses.js @@ -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' ); diff --git a/src/getID.js b/src/getID.js index 0fe9cc9..bbe032a 100644 --- a/src/getID.js +++ b/src/getID.js @@ -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 )}`; } diff --git a/src/getName.js b/src/getName.js index 722c9e6..8d10f70 100644 --- a/src/getName.js +++ b/src/getName.js @@ -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}"]`; } diff --git a/src/index.js b/src/index.js index 894a433..d3c7285 100644 --- a/src/index.js +++ b/src/index.js @@ -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 = [] @@ -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 @@ -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 ) { @@ -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'; } } @@ -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} 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} 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} @@ -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 @@ -216,7 +228,7 @@ export default function unique( el, options={} ) { currentElement, selectorTypes, attributesToIgnore, - filters + normalizedFilter ) if (selectorCache) { selectorCache.set(currentElement, selector) diff --git a/test/unique-selector.js b/test/unique-selector.js index b5bfe29..d9e7332 100644 --- a/test/unique-selector.js +++ b/test/unique-selector.js @@ -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( '
' )[0]; $(el).appendTo('body') - let uniqueSelector = unique( el, { filters } ); + let uniqueSelector = unique( el, { filter } ); expect( uniqueSelector ).to.equal( '#foo' ); el = $.parseHTML( '
' )[0]; $(el).appendTo('body') - uniqueSelector = unique( el, { filters } ); + uniqueSelector = unique( el, { filter } ); expect( uniqueSelector ).to.equal( 'body > :nth-child(2)' ); }); @@ -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( '
' )[0]; $(el).appendTo('body') - let uniqueSelector = unique( el, { filters } ); + let uniqueSelector = unique( el, { filter } ); expect( uniqueSelector ).to.equal( '.a1' ); el = $.parseHTML( '
' )[0]; $(el).appendTo('body') - uniqueSelector = unique( el, { filters } ); + uniqueSelector = unique( el, { filter } ); expect( uniqueSelector ).to.equal( '.a2' ); }); @@ -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)' ); @@ -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( '
' )[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( '
' )[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' ); }) }); @@ -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( '
' )[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( '
' )[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' ); }) }) @@ -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( '
' )[0]; $(el).appendTo('body') - let uniqueSelector = unique( el, { filters } ); + let uniqueSelector = unique( el, { filter } ); expect( uniqueSelector ).to.equal( '[name="abc"]' ); el = $.parseHTML( '
' )[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( '
' ); + 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( '
' )[0]; + const findNode = $( 'body' ).find( '.test-nth-child' ).get( 0 ); + const uniqueSelector = unique( findNode, { filter, selectorTypes : ['nth-child', 'tag'] } ); + expect( uniqueSelector ).to.equal( 'span' ); + }) + }) } );