Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Sara Dunnack
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Sara Dunnack

jQuery Plugin: Tracing Your Selector Paths

By
Published in Comments (10)

A while back, I wrote a proof-of-concept about enabling step-debugging in your jQuery selector paths. In that previous demo, you had to enter your jQuery selector values into a form and execute it manually within the test environment. This morning, I took that same concept and rolled it into a jQuery plugin. The plugin both lives as a utility function off of the jQuery library as well as a function that extends the jQuery collections (which, internally, just calls the utility method). The way the tracer works is that it starts at the top of the selector path and walks through each selector step, adding a CSS class to each set of resultant nodes, giving them each a successively darker background color.

As you can see in the video, the mid-steps are gray and the final step is gold, indicating document object model (DOM) nodes that will be present in the final jQuery collection. The tracer can be run as a method off of the jQuery library:

$.trace( "p strong" );

... or it can run off of a jQuery collection:

$( "p strong" ).trace();

In the latter case, the trace() plugin calls the utility method, using the "selector" property of the current collection as the selector path to trace. When called as a plugin, the trace() method returns a refernce to the current collection so that this can be used for debugging without breaking the Javascript execution work flow.

This plugin is for debugging purposes - there is no use case outside of that (just in case there was any confusion as to how this would be applied to a production application - it wouldn't be). If anyone is interested in it, here is the jQuery Javascript file that I wrote:

jquery.trace.js

// Create a closure and a self-executing method so that the
// trace method can create variable dependencies without
// putting things in the global name space.
(function( $ ){

	// This is the original selector sent to the tracer.
	var originalSelector = null;

	// This is the collection of selector parts that the parser
	// was able to break out of the original selector.
	var selectorMaps = [];

	// This is the current depth of the tracing.
	var traceDepth = 0;

	// This is the timeout reference being used to get from one
	// selector text to the next.
	var traceTimeout = null;

	// This is the amount of time that the trace will pause
	// between steps (this can be overriden later on when the
	// trace method(s) are called.
	var defaultPause = (2 * 1000);

	// This is the pause value that the actual trace will use.
	var tracePause = defaultPause;


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// I check to make sure the styles needed by the tracer
	// have been injected into the document if they are not
	// already there.
	var checkForStyles = function(){
		var tracerStyle = $( "#jquery-tracer-style" );

		// Check to see if the node was found.
		if (!tracerStyle.size()){

			// The node was not found, so we have to inject it
			// into the head.
			$( "head" ).append("\
				<style id=\"jquery-tracer-style\">\n\
				.jqst0 { background-color: #F0F0F0; }\n\
				.jqst1 { background-color: #D0D0D0; }\n\
				.jqst2 { background-color: #B0B0B0; }\n\
				.jqst3 { background-color: #909090; }\n\
				.jqst4 { background-color: #707070; }\n\
				.jqst5 { background-color: #505050; }\n\
				.jqst6 { background-color: #303030; }\n\
				.jqst7 { background-color: #151515; }\n\
				.jqstX { background-color: gold; }\n\
				</style>"
				);

		}
	};


	// This removes all tracer styles from the document.
	var removeStyles = function(){
		// Loop over each style class and remove it from any
		// matching nodes.
		for ( var i = 0 ; i <= 7 ; i++ ){

			// Remove the styles from class-matching nodes.
			$( ".jqst" + i ).removeClass( "jqst" + i );

		}

		// Remove any final styles.
		$( ".jqstX" ).removeClass( "jqstX" );
	};


	// I clean the given selector, preparing it for parsing.
	var cleanSelector = function( selector ){
		// First, let's just trim it.
		selector = $.trim( selector );

		// Replace multiple spaces with a single space.
		selector = selector.replace(
			new RegExp( " +", "g" ),
			" "
			);

		// Join the special descendent selector to the seelctor
		// that it references (this will make parsing easier).
		selector = selector.replace(
			new RegExp( "([>~]) +", "g" ),
			"$1"
			);

		// Return the scrubbed selector.
		return( selector );
	};


	// I get the top-level selectors from the given selector
	// string. By that, I mean that I return sets of selectors
	// that are separated by commas (,) in a single selector.
	var getTopLevelSelectors = function( selector ){
		// Get the selectors separated by commas.
		var topLevelSelectors = selector.match(
			new RegExp(
				"([^,'\"]+('[^']*'|\"[^\"]*\")?)+",
				"gi"
				)
			);

		// Return the array of top level selectors.
		return( topLevelSelectors );
	};


	// I parse the selector into it's given step-wise parts. The
	// return value will be an array of top-level selectors that
	// has each been broken down into an array of path steps.
	var parseSelector = function( selector ){
		// Get the top level selectors.
		var topLevelSelectors = getTopLevelSelectors(
			cleanSelector( selector )
			);

		// Create an array to hold the individual paths of
		// top-level selectors. For each top level selector,
		// parse its path into individual step-wise parts.
		var selectorMaps = [];

		// Get a body node - this will be the root context of each
		// selector path.
		var jBody = $( document.body );

		// Iterate over each top level selector.
		$.each(
			topLevelSelectors,
			function( index, topLevelSelector ){

				selectorMaps.push({
					context: jBody,
					selectors: $.trim( topLevelSelector ).match(
						new RegExp(
							"("+
								"[^\\s'\"(]+" +
								"(" +
									"'[^']*'" + "|" +
									"\"[^\"]*\"" + "|" +
									"\\(" +
										"(" +
											"[^)'\"]*" +
											"('[^']*'|\"[^\"]*\")?" +
										")*" +
									"\\)" +
								")?" +
							")+",
							"gi"
							)
						)
					});
			}
			);

		// Return the collection of top level selector paths
		// that have been broken down into parts.
		return( selectorMaps );
	};


	// I reset the tracer environment for a new trace.
	var reset = function(){
		// Remove all the style.
		removeStyles();

		// Reset properties.
		originalSelector = null;
		selectorMaps = [];
		traceDepth = 0;
		clearTimeout( traceTimeout );
		traceTimeout = null;
	};


	// I perform the trace at the current depth. I rely on the
	// globally defined variables to perform the trace.
	var executeTrace = function(){
		var map = null;
		var keepTracing = false;

		// Loop over each map to highlight the next trace in our
		// breadth-first trace algorithm.
		for (
			var mapIndex = 0 ;
			mapIndex < selectorMaps.length ;
			mapIndex++
			){

			// Get a handle on the current path.
			map = selectorMaps[ mapIndex ];

			// Check to see if there is an element at this depth
			// in the current path.
			if (map.selectors.length > traceDepth){

				// Update the context for this map.
				map.context = map.context.find(
					map.selectors[ traceDepth ]
					);

				// Apply classes to the new context.
				map.context.addClass( "jqst" + traceDepth );

				// Check to see if this particular map has any
				// more steps in it. If it does, then flag the
				// tracing for continuation. If it does not,
				// then let's add the final step class.
				if (map.selectors.length > (traceDepth + 1)){

					// More steps.
					keepTracing = true;

				} else {

					// This is the final step, flag the the
					// current context as final.
					map.context.addClass( "jqstX" );

				}
			}

		}

		// Increase the trace depth.
		traceDepth++;

		// Check to see if we should keep tracing.
		if (keepTracing){

			// Set a timeout for the next trace.
			traceTimeout = setTimeout(
				executeTrace,
				tracePause
				);

		}
	};


	// Add the trace method to the jQuery method.
	$.trace = function( selector, pause ){
		var selector = selector;

		// Before we begin, check to make sure that the classes
		// we need have been injected into the DOM.
		checkForStyles();

		// Reset the testing environment.
		reset();

		// Store the original selector.
		originalSelector = selector;

		// Store the selector tree.
		selectorMaps = parseSelector( selector );

		// Set up the tracing pause time.
		tracePause = (pause || defaultPause);

		// Execute the trace.
		executeTrace();
	};


	// Add a trace method as a plugin.
	$.fn.trace = function( pause ){
		// Simply pass the original selector off to the
		// core trace method.
		$.trace( this.selector, pause );

		// Return the current stack.
		return( this );
	}

})( jQuery );

Because the trace() method is stateful, only one trace can be run at a time. Any attempt to start a second trace while an existing trace is running, will stop the first trace, reset the testing environment, and start the second trace.

Want to use code from this post? Check out the license.

Reader Comments

2 Comments

hi ben,
i do like your plugin very much as it is very useful in my opinion!
as i have to use very often the $.click event combined with an alert to check if i hit the right element, i ll definitely use this plugin in future!
br p

1 Comments

Thanks so much for coding this. I'm finding it tremendously useful since, for some reason, selector syntax continues to be one of the more obscure aspects of jquery for me. This should definitely help.

1 Comments

@ano & @ben
The error comes from some stray markup in the download packages code. Look at the regex in the parseSelector function. In the zip, there is some html in the middle of it, but it isn't there in the blog posting code. Not sure if it is the only issue, but it covers that particular error.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel