AutoPlayDom

The AutoPlayDom widget is a tool that I have been developing to perform step by step DOM actions. The primary purpose of this tool is for debugging, so that a developer can play through the classes/styles that are applied at various times throughout the life of a webpage. Then the developer can view the pages in their graded-browser support matrix and verify that the everything appears as expect. However, the tool is flexible enough to perform a serious of DOM manipulations, which have the effect of animating a page (although it does not leverage an animation library, yet…).

The project file is available at http://www.mattsnider.com/assets/js/widget/AutoPlayDom.js.

Todays article will cover the two main (and most complicated) public methods of AutoPlayDom: add and next. The add function allows more steps to added to the _data array, which holds the logic for each step of DOM manipulation. The next function performs the required tasks found in each step.

Example 1: AutoPlayDom.add()

add: function() {
	// iterate on the arguments
	for (var i = 0; i < arguments.length; i+= 1) {
		var o = arguments[i];

		if (_YL.isArray(o)) {
			_that.add.apply(_that, o);
		}
		else if (o && o.name && o.tasks && o.tasks.length) { // validate 
			_data.push(o);
		}
		else {
			alert(attempting to add invalid object);
		}
	}
},

The add method in Example 1 uses the ArgumentOrArray Pattern, where the developer can pass in the tasks as: a single task object, many task objects as parameters, an array of many task objects, or many arrays containing task objects and/or task objects as parameters. The task objects will be added by their argument position first, then there position in the array. So the following:

Example 2: using AutoPlayDom.add()

var apd = Core.Widget.AutoPlayDom();
apd.add(object1);
apd.add([object2, object3], object4, [object5]);

will play out in this order: object1, object2, object3, object4, object5. Lastly, the add method validates that the task object has set name and tasks is an array. This will allow the next method to run properly for this task object. Task objects should be of the form shown in Example 3.

Example 3: Task Object

{name: The Name of This Task, tasks: [
	{search: [ // an array of search objects
		{method: name of a method attach to YAHOO.util.Dom, params: [/*an array of parameters to pass into method*/]}
		// each subquent object will use the resulting node or nodes from the previous search as their root parameter
	], actionMethod: name of a method attach to YAHOO.util.Dom, actionParams: [ // array of actions parameters to apply to the node, using this actionMethod
		[/* array of parameters to use with actionMethod */]
	]},

	{search: [
		{method: getElementsByClassName, params: [testClass, form, document.body]},
		{method: getElementsBy, params: [fieldset]}
	], actionMethod: setStyle, actionParams: [
		[height, 100px], // use setStyle to apply the height to the node
		[width, 300px], // use setStyle to apply the width to the node
		[z-index, 100] // use setStyle to apply the zIndex to the node
	]}
]}

The comments in Example 3 explain how to use each part of the task object. Once added, the task objects can be executed using the next method.

Example 4: AutoPlayDom.next()

next: function() {
	// go to the next position
	if (_that.hasNext()) {
		_index += 1;

		var o = _data[_index];

		// iterate on the tasks
		_array_walk(o.tasks, function(t) {
			var isValidSearch = t.search && t.search.length,
				isValidAction = t.actionMethod && t.actionParams && t.actionParams.length,
				nodes = null;

			if (isValidSearch && isValidAction) {
				// iterate on the search methods
				_array_walk(t.search, function(o) {
					if (nodes) {
						var new_nodes = [];
						
						// iterate on the nodes and apply the search method
						_array_walk(nodes, function(node) {
							var fx = _YL.isFunction(o.method) ? o.method : _YD[o.method];
							
							if (fx) {
								var new_params = [];

								if (o.params) {
									new_params = _YL.isArray(o.params) ? o.params : [o.params]; // normalize params
								}

								// various need the node placed in differing parameter locations
								if (_YL.isString(o.method)) {
									switch (o.method) {
										case getElementsByClassName: // these may need to be updated, anytime listOfMethods in loadAutoPlayHelperFunctions is changed 
										case getElementsByTagName: // this group of functions puts the node at the end
										case getElementsBy:
											new_params.push(node);
										break;

										default: // this group of functions puts the node at the beginning
											new_params = [node].concat(new_params);
										break;
									}
								}
								else {
									new_params = [node].concat(new_params);
								}

								var rs = fx.apply(window, new_params);

								if (rs) {
									if (_YL.isArray(rs)) {
										new_nodes = new_nodes.concat(rs);
									}
									else {
										new_nodes.push(rs);
									}
								}
								else {
									// this index will be removed
								}
							}
							else {
								throw(EXCEPTION: AutoPlayDom.Next - Invalid method used ( + o.method + ); no elements found);
							}
						});

						nodes = new_nodes;
					}
					else {
						var fx = _YL.isFunction(o.method) ? o.method : _YD[o.method];
						nodes = fx.apply(window, _YL.isArray(o.params) ? o.params : [o.params]);
						if (! _YL.isArray(nodes)) {nodes = [nodes];} // convert to an array so we can do same logic, when array or not array
					}
				});

				var actionMethod = _YL.isFunction(t.actionMethod) ? t.actionMethod : _YD[t.actionMethod];

				if (actionMethod) {
					// iterate on the nodes and apply the action method
					_array_walk(nodes, function(node) {
						if (! _YL.isArray(t.actionParams)) {t.actionParams = [t.actionParams];} // convert to an array so we can do same logic, when array or not array

						_array_walk(t.actionParams, function(params) {
							actionMethod.apply(window, [node].concat(params));
						});
					});
				}
				else {
					throw(EXCEPTION: AutoPlayDom.Next - Invalid actionMethod used ( + t.actionMethod + ); method not found on YAHOO.util.Dom);
				}
			}
			else {
				throw(EXCEPTION: AutoPlayDom.Next - Invalid task used (isValidSearch: + isValidSearch + , isValidAction: + isValidAction + ));
			}
		});
	}
},

The next method in Example 3 moves the DOM state from the current task object to the next. The next method first validates that there is another task object available, then walks through each task on the task object. These tasks are checked if they contain a search array, an actionMethod, and an array of parameter arrays (actionParams). Next each object is search is iterated on. Each search object should contain a method and an array of parameters (params). If there is only one search object, then the method moves forwarding using the node(s) found. However, when there are more than one search objects, the method iterates through nodes, calling the new search methods using the previously found node(s) as a the root node. Any method found this way will replace the values previously in the nodes list. There is some special-case logic for various methods where the root node is the last parameter, instead of the first parameter, but the default is to use the previous node as the first parameter, when calling the search method.

Once all the nodes are found and the actionMethod is defined, the nodes are iterated on for DOM manipulation. The actionMethod can be found on "YAHOO.util.Dom" by default (such as setStyle or addClass) or a function of the developers own creation. It is assumed for these methods that the node should be the first parameter. Lastly, for each node, the actionParam array is iterated on, to call the actionMethod with the node and each actionParam.

This tool is versioned 0.5, because I intend to write a previous function which will be a dynamic way to iterate backwards through DOM actions that were previously executed in the forward direction. This is going to be very tricky and I did not have time for it this week. When I finish the previous method I will create a test page so that you can experiment with AutoPlayDom.