A JavaScript, CSS, and XHTML Resource By Matt Snider

Understanding JavaScript and Frameworks

Tuesday, February 10, 2009

Library - YUI - Augmenting Cookie

Over the past week, I have had the opportunity to explore the YUI Cookie object. It has all the functionality from the 2007 article on Cookies, with the addition of sub cookies. Sub-cookies get around the maximum number of available cookie limitation, by allow the developer to store multiple cookies inside of one cookie. This is especially useful for sub-domains, see YUI -Cookie for more information.

However, there are a few methods that were missing: ‘getNumberOfCookies’, ‘getCookieSize’, and ‘isCookiesEnabled’. The ‘getNumberOfCookies’ function returns the number of cookies currently set; simply splitting around ‘;’ seems to work in the browsers I tested. Let me know if there is a better way or browser issues I missed. The ‘getCookieSize’ method does a pretty good estimation of the cookie size, by assuming all alpha-numeric characters are not escaped and thereby stored as 1 byte and all non-alpha-numeric characters are escaped and thereby stored as 3 bytes. Although, the later is not always true, it is a fairly accurate assumption; I am open to a better regex that considers the other characters which are not escaped. Lastly, the ‘isCookiesEnabled’ enabled function determines if Cookies are enabled, first by looking at the ‘navigator’ object, then by checking if there is a cookie set, and lastly by adding a test cookie.

Example 1: Augmenting Methods

(function() { var _YC = YAHOO.util.Cookie, _OBJ = { /** * Returns the number of cookies currently used. * @method getNumberOfCookies * @return {Number} The number of cookies. * @private */ getNumberOfCookies: function() { return ('' + document.cookie).split(';').length; }, /** * Estimates the size of the cookie using 1 byte for each alpha-numeric character and 3 for each non-alpha-numeric character. * @method getCookieSize * @param key {String} Required. The cookie key to test. * @return {Number} The estimated cookie size. * @private */ // note: this method has been improved, see Improved Cookie Size Calculation getCookieSize: function(key) { var str = ('' + _YC.get(key)), strAlphaNum = str.replace(/[\W+]/g, ''); return strAlphaNum.length + ((str.length - strAlphaNum.length) * _SPECIAL_CHAR_BYTE_SIZE); }, /** * Tests if cookies are enabled. * @method isCookiesEnabled * @return {Boolean} True when cookies enabled; * @private */ isCookiesEnabled: function() { var testName = 'YAHOO.util.Cookie', testValue = 'test'; if (navigator && ! navigator.cookieEnabled) {return false;} // navigator tells us no if (('' + document.cookie).length) {return true;} // cookies are avialable, assume enabled _YC.set(testName, testValue); if (testValue === _YC.get(testName)) { _YC.remove(testName); return true; } else { return false; } } }; YAHOO.lang.augmentObject(_YC, _OBJ); })();

With these methods included in addition to YUI, you can determine if you should use cookies to store user information (using ‘isCookiesEnabled’) and parse large data between multiple cookies (using ‘getNumberOfCookies’ and ‘getCookieSize’). The worst case scenario will be that each cookie can not be larger than 4000 bytes and that you can not have more than 20 cookies per domain.

posted by Matt Snider at 10:42 pm  

Sunday, February 8, 2009

Project - DomTaskPlayer

This is a continuation of the article Project - AutoPlayDom. The project was renamed to DomTaskPlayer, because the new name is semantically more correct, as the object plays through a series of Dom related tasks. The full source code is available at http://www.mattsnider.com/assets/js/widget/DomTaskPlayer.js. The source code has drastically changed from last week, as supporting backwards navigation through the already played DomTasks, required a massive redesign of the code.

For starters the “DomTaskPlayer.add” method has changed, to accept a collection of ’step’ objects, which will contain ‘task’ objects, which will contain ‘action’ objects:

Example 1: A Step Object

var step = { _isValid: false, // used internally, updated after first 'next' call name: '', // what this step is tasks: [ // an array of tasks { // task action: [ {fx: function() {}, method: '', params: [], reverseMethod: '', reverseFx: function() {}}, // action ... ], search: [ {fx: function() {}, method: '', params: []}, // action ... ], reverse: [] // internally managed }, ... ] };

A ’step’ object is a collection of ‘task’ objects and a ‘name’ to label the step. The ‘task’ object is a collection of search ‘action’ objects (to find the node(s) to apply actions to), and a collection of action ‘action’ objects (actually describing how to change the nodes found when searching). There is no limit to the number of tasks per step and actions per task. When the “DomTaskPlayer.next” method is called an internal collection of ‘action’ objects are attached to the reverse list, explaining how to reverse the current step.

The ‘next’ and ‘previous’ methods have been simplified, delegating the work to smaller more targeted internal assertion and action functions.

Example 2: DomTaskPlayer.next

next: function() { // go to the next position if (_that.hasNext()) { _index += 1; var step = _steps[_index]; // already validated and found nodes, no need to assert if (step._isValid) { _array_walk(step.tasks, function(task) { // iterate on the tasks _array_walk(task.action, _execute_action); // iterate on the actions }); } else { _array_walk(step.tasks, function(task) { // iterate on the tasks _assert_valid_task(task); var tobj = {}; _array_walk(task.search, _execute_search, tobj); _array_walk(task.action, function(action) { // iterate on the actions _assert_valid_action(action); // validates and/or sets 'action.fx'; assures action.params is an array _assert_user_reversible(action); // user fx is reversible action.nodes = tobj.nodes; _execute_action(action); }); }); step._isValid = true; } _that.onStepChange.fire([step, 'next']); } },

This method first asserts that there is a next step available, then grabs the step from the internal step array. If the step has the internal variable ‘_isValid’ set to true, then the method knows that it has previously already validated this step and can skip the search actions, executing the action tasks on cached DOM nodes. Otherwise, it iterates through each task and asserts that the tasks are valid. Afterwards, it searches the DOM using the search ‘action’ objects, producing an object with key ‘nodes’. Then it iterates on the action ‘action’ objects and asserts they are valid and reversible. If no errors have been thrown, terminating the flow, then the method actually executes the ‘action’ objects. As each ‘action’ object is executed, the internal methods create the reverse actions, which will be used with the ‘previous’ method.

In addition, to supporting ‘previous’ and the massive revisions to ‘next’, there is now a build-in, public method ‘initKeyListeners’ that hijacks the arrow keys (and ‘a’ - left arrow, ’s’ - down arrow, ‘d’ - right arrow, ‘w’ - up arrow) to call one of the public action methods of DomTaskPlayer (’previous’, ’stop’, ‘next’, ‘play’). The developer can require that any combination of ‘Ctrl’, ‘Alt’, or ‘Shift’ be pressed as well, so not to be obtrusive.

Example 3: DomTaskPlayer.initKeyListeners

initKeyListeners: function(forceCtrl, forceShift, forceAlt) { _YE.on(document, 'keydown', function(e) { if (forceCtrl && ! e.ctrlKey) {return;} // may trigger browser events if (forceShift && ! e.shiftKey) {return;} if (forceAlt && ! e.altKey) {return;} _YE.preventDefault(e); // stop browser events switch (_YE.getCharCode(e)) { case _YK.DOWN: case 83: // s _that.stop(); break; case _YK.LEFT: case 65: // a _that.previous(); break; case _YK.RIGHT: case 68: // d _that.next(); break; case _YK.UP: case 87: // w _that.play(); break; default: // do nothing } }); _that.initKeyListeners = function() {}; },

At the end of ‘initKeyListeners’ the method is set to empty function, thereby preventing the developer from accidentally attaching these key listeners more than once. This method can be called anytime after the DomTaskPlayer object has been initialized and it is recommended that at least two special keys be set to true, such as ‘Shift’ and ‘Ctrl’:

Example 4: Calling initKeyListeners

yourDomTaskPlayerObject.initKeyListeners(true, true);

In Example 4, the user will need to hold ‘Shift’ + ‘Ctrl’ + ArrowKey to trigger the desired method.

Most importantly, this tool is easy to use; see Dom Task Player Test Page. Although easy to use, this tool is very complicated, please let me know if something does not work for you or are having trouble getting it working.

Lastly, please keep in mind that order of operations are very important with tasks, so if you first apply a class that changes the background, then change the background with a style directly in the same ’step’ object, then the reverse operation will be confused and apply the wrong background color.

posted by Matt Snider at 8:03 pm  

Thursday, February 5, 2009

Architecture - Validating Strings and Unsigned Numbers

One of the frequently overlooked JavaScript development practices is to validate the parameters passed into functions. Today’s articles will look at two ways to validate string and an unsigned number parameters.

Example 1: Safe String Operation

var str_trim = function(s) { ('' + s).remove(/^\s\s*/).remove(/\s\s*$/); };

Anytime a string is expected as a parameter, the developer has two approaches for validation: using a type detection function/comparison or simply ensuring that the parameter is a string using the concatenation method shown in Example 1. In JavaScript, if a developer needs a string, they simply need to concatenate an empty string onto the parameter and any object will be converted into a string. So in this case, no matter what is passed into the ’str_trim’ function, the function will not throw any exceptions due to ’s’ not being a string and not having the ‘remove’. A verbose developer can use this technique anytime they are using a string method on a variable, although large strings can slow down performance.

Example 2: Unsigned Number

var number_random_int = function(n) { var seed = (0 < n) ? n : 100; return parseInt(Math.random() * seed); };

In Example 2, the developer is expecting that parameter ‘n’ is a positive number. The method ‘number_random_int’ returns a random number between 0 and ‘n’, but requires that ‘n’ be positive, otherwise, it will default to 100. Because JavaScript autocasts variables, any non-number object passed into the function will be determined as NaN (Not A Number) and ignored by the comparison, but any number (or number as a string) provided will validate (this can be a gotcha with this technique as you are not validating that ‘n’ is a number, just that it evaluates as a positive integer; so don’t use this technique, if you plan on concatenating ‘n’). The only other object that will also resolve to a number is the Date object, which will resolve to the ‘getTime’ of the date.

————————-

In other news, I have the DomAutoPlay function working both forwards and backwards now. However, it required a massive overhaul of the version that I wrote last week and I plan to write about it this weekend.

posted by Matt Snider at 12:48 am  

Saturday, January 31, 2009

Project - 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.

Today’s 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.

posted by Matt Snider at 5:23 pm  

Thursday, January 29, 2009

X-Browser - FireFox - Permission Denied to set property XULElement.selectedIndex

Sorry for the late post this week. I have been working hard on the YUI storage project and the new release for Mint.com and did not finish my post in time. Todays article covers an issue you may experience when using the native DOMElement ‘focus’ and ’select’ functions in FireFox.

If you have FireBug running, the exception is:

“Permission denied to set property XULElement.selectedIndex”

followed by a pointer to the offending line.

The issue is that when you use “element.focus()” and/or “element.select()”, FireFox attempts to manipulate the built in autocomplete feature. However, it does not always do that correctly. I am not exactly sure what is going on in the FireFox code, but it seems to only happen on fields where the built in browser autocomplete was previously triggered. As long as you do not care about the built in FireFox autocomplete, the fix is very easy, simply add the attribute ‘autocomplete’ and give it a value of ‘false’.

Example 1: In HTML

<input autocomplete="off" type="text" value"some text"/>

Example 2: In JavaScript

element.setAttribute('autocomplete', 'off');

Example 1 will cause any HTML validator to fail, so you may want to set this attribute via JavaScript (Example 2). Example 2 will fix the issue in FireFox and simply be ignored by other browsers.

posted by Matt Snider at 6:40 pm  

Saturday, January 24, 2009

Library - Dojo - Provide

Before I get into the article today, some house-keeping. Sorry, to miss the last mid-week article, I have been working really busy with the next version of Mint.com and have been diligently working on porting >dojo.storage. Both projects are progressing nicely, but not as quickly as I had hoped; I will keep you abreast of any progress.

Anyway, while working with Dojo, I took a look under the hood of its namespacing function, “dojo.provide(namespace)”. The method registers the namespace with an internal object, before returning a function object to the caller. This internal object allows for quick detection to see if a given namespace exists. Here are two simple methods that will accomplish the same:

Example 1: Provide

var mesnider_provide = function(s) { var list = ('' + s).split(/\./), sb = [], scope = window; if (! mesnider_provide._provided) {mesnider_provide._provided = {};} for (var i = 0; i < list.length; i += 1) { var name = list[i]; if (! scope[name]) { sb[i] = name; scope[name] = function() {}; mesnider_provide._provided[sb.join('.')] = scope[name]; } scope = scope[name]; } return scope; };

Example 2: Is_Provided

var mesnider_is_provided = function(s) { return mesnider_provide._provided['' + s]; };

The ‘provide’ function in Example 1, splits the provided string parameter around all periods (’.'). This creates a list that can be iterated on to traverse the namespaces for creation. The ‘_provided’ object is added to the function itself, if it does not already exist, to be used with ‘is_provided’ in Example 2 for fast detection of namespaces. All namespaces are attached starting from the ‘window’ namespace and returned as an a function (unless already defined as something else). The ’sb’ array is used as a string buffer when adding to the ‘_provided’ object, so that the whole namespace up to that point can be added. Lastly, the scope is adjusted for the next iteration and also so it can be returned at the end of the ‘provided’ method (this will be the newly created object, or the namespace previously created).

The ‘is_provided’ method needs only to check if the provided namespace exists in the “mesnider_provide._provided” object. Although, it does not return a boolean, instead it returns a truthy value, the namespace object itself, when it exists, or ‘undefined’ otherwise. The method was coded this way, because there are times that a developer has the namespace string and desires to have the object, but has to convert the string into the object. This tool prevents the need for that conversion, improving code performance.

posted by Matt Snider at 11:27 am  

Friday, January 16, 2009

Library - Dojo - Event.Connect Lite

While working on porting “Dojo.Storage” to YUI, I took an in-depth look at the Dojo.Event.Connect method. This is the backbone to many Dojo applications, allowing developers to subscribe to browser events, Dojo events, and to attach functions to be fired after the execution of other functions. And it is this last feature that inspired todays article. Sometimes a developers needs Function B to fire after Function A. Most of the time the developer needs to create a closure to manage the link and then call the closure in place of Function A. However, a better approach is to replace Function A with the closure function, so that no code needs to be changed to call the special closure.

Example 1: mesnider_bind_function

var mesnider_bind_function = function(obj1, func1, obj2, func2, execNum) { if (! obj1) {obj1 = YUI();} if (! obj2) {obj2 = YUI();} if (! (obj1[func1] || obj2[func2])) {return;} // one of the functions doesn't exist, no reason to continue var TEMP_FUNC_NAME = func1 + '$oldFunc'; var execn = 0; obj1[TEMP_FUNC_NAME] = obj1[func1]; obj1[func1] = function() { obj1[TEMP_FUNC_NAME].apply(this, arguments); obj2[func2].apply(this, arguments); execn += 1; if (execNum && execn >= execNum) { obj1[func1] = obj1[TEMP_FUNC_NAME]; delete obj1[TEMP_FUNC_NAME]; } }; };

The ‘mesnider_bind_function’ function requires 4 parameters, and 1 optional parameter: the first parameter is the object that the second parameter (the function name as a string) is attached to (the YUI object is used if this object is not provided), parameter three and four are the same structure as one and two, but is the second object and function, and the optional fifth parameter is the limit of times ‘func2′ should be executed when ‘func1′ is called. The second parameter ‘func1′ is the function to be called by code, while the fourth parameter ‘func2′ will be the function executed whenever ‘func1′ is called.

To bind ‘func2′ to ‘func1′, ‘func1′ must first be moved to a temporary location (func1 + ‘$oldFunc’) on ‘obj1′ so that it can be called and restored (if necessary). Then ‘func1′ is replaced with a new function that will call both ‘func1′ and ‘func2′, passing any arguments provided to the new ‘func1′ into both ‘func2′ and the old ‘func1′, and iterate the execution counter (’execn’). If a maximum execution number (’execNum’) was passed to ‘mesnider_bind_function’ function then it will be compared against the execution counter (’execn’). When the execution counter is equal to or greater than the maximum execution number, then ‘func1′ is restored to its previous value and the function binding is removed.

This method simplifies associating the execution of Function B to Function A, opening many possibilities for extending the capabilities of functions, without overriding them. Here is a simple test page to show 3 different ways to use this method.

posted by Matt Snider at 10:46 am  

Wednesday, January 14, 2009

Widget - Highlight On Change

One of the more subtle, but very useful form feedback tools, is to animate the background when a user has changed a field. This animation is mostly useful with ‘text’ typed inputs, however, there is a case to be made about ‘textarea’ as well. Today’s article shows a singleton method that manages a highlight animation, triggered whenever a user changes a text field. The developer need only register the field and the ChangeHighlighter widget will take care of the rest.

Example 1: ChangeHighlighter Singleton

Core.Controller.ChangeHighlighter = (function() { // local namespace var _F = function() {}, _that = null, _YUD = YAHOO.util.Dom, _YUE = YAHOO.util.Event; // public namespace _F.prototype = { /** * Registers the input with this widget. * @method register * @param elem {Element} Required. The input element to listen on. * @param conf {Object} Optional. Configuration options. * @static */ register: function(elem, conf) { var npt = _YUD.get(elem); // valid input if (npt && npt.type && 'text' === npt.type) { var cfg = conf ? conf : {color: '#EE0'}, lastValue = npt.value; if (! cfg.duration) {cfg.duration = 0.75;} _YUE.on(npt, 'blur', function() { var val = (cfg.trim && ''.trim) ? npt.value.trim() : npt.value; npt.value = val; if (lastValue === val) { // no change, do nothing } // value changed, animate else { lastValue = val; var obgColor = _YUD.getStyle(npt, 'background-color'), anim = new YAHOO.util.ColorAnim(npt, {backgroundColor: {to: cfg.color}}, cfg.duration); anim._onComplete.subscribe(function() {_YUD.setStyle(npt, 'background-color', obgColor);}); anim.animate(); } }); } else { alert('ChangeHighlighter:add Error - Invalid input parameter provided.'); } }, /** * Registers the input with this widget. * @method register * @param elem {Element} Required. The form element to search. * @param conf {Object} Optional. Configuration options. * @static */ registerForm: function(elem, conf) { var form = _YUD.get(elem); // valid element if (form) { var npts = form.getElementsByTagName('input'); // iterate on inputs and register for (var i = 0; i < npts.length; i += 1) { if ('text' === npts[i].type) { _that.register(npts[i], conf); } } } } }; _that = new _F(); return _that; })();

All the work is done inside of the static “Core.Controller.ChangeHighlighter.register” method, so that a closure can be leveraged each time the ‘blur’ event is subscribed to. The ‘register’ method requires two parameters: the input to listen on and an optional configuration. The configuration allows the developer to change the animation color, the duration, and whether or not the values of the inputs should be trimmed before comparing (this also requires that “String.prototype” has been extended with a trim method). A variable ‘lastValue’ is used to track the previous value of the input so that the ‘blur’ event callback knows that a change has occurred (this is why a closure is needed). Then inside the ‘blur’ event callback, the ‘lastValue’ is compared with the current value, and a color animation is fired if the values are not the same.

There is also a ‘registerForm’ method, which simply searches for ‘text’ inputs and then calls the register method for each. I avoided using any non-YUI methods for ease of use.

posted by Matt Snider at 10:21 am  

Thursday, January 8, 2009

Widget - Animation Blind Singleton

A quick apology for not getting this article out sooner. I was having issues with wordpress: it lost part of the posting and repeatedly choked on the code sample. Anyway, sorry for the delay.

Today’s article combines the Dom Search Experiment, discussed last week, with the Event Dispatcher, discussed last month, and YUI. The purpose of this widget is to allow designers to have complete control over which anchors trigger blind animations and what elements are associated with those triggers (ie. what element will animate), without having to write a single line of JavaScript.

To accomplish this goal the Blind Animator needs to be a standalone singleton object that responds to some design driven animation trigger. Event dispatcher is the perfect tool for this, because no events need to be attached to the anchors themselves, as a master click event is attached to the page. This master event will then dispatch to registered callback functions as necessary. The Singleton object need only register a callback on the generic trigger, ‘animBlind’, which will map to the class ‘com_animBlind’. So any anchor element on a page with the ‘com_animBlind’ class will trigger a blind animation.

Since the Blind Animation Singleton will not know anything about the trigger anchor or what element should be animated, there needs to be a way to associate the anchor with the element to animate. For this the ‘rel’ attribute of an anchor tag is used and the Dom Search Experiment (’exec’ function). The ‘rel’ attribute will contain the instructions string to be passed into the ‘exec’ function. This way the designer can specify how to find the node to animate.

Lastly, a blind animation must be setup on the node to animate. For starters this means using the following DOM and styles:

Example 1: Styles And DOM Required for Blind Animation

.blindWrap { height:0; overflow:hidden; } <div class="blindWrap"><div>Enter your Content HERE! This inner node needs to be displayed and have layout.</div></div>

The wrapping node ‘blindWrap’ will have a height of ZERO and overflow hidden, so that its content is not displayed. However, the content itself (its first child) should be displayed normally so that it can be used to find the height dimension for the blind animation.

Putting this all together, is the Blind Animation Singleton object:

Example 2: Blind Animation Singleton

Core.Controller.AnimBlindSingleton = (function() { // constants var _CLASS_OPEN = 'open', _COM_ANIM_BLIND = 'animBlind', YUE = YAHOO.util.Event; // local namespace var _CE = YAHOO.util.CustomEvent, _ED = Core.Util.EventDispatcher, _F = function() {}, _isAnimating = false, _queueAnim = [], _that = null; var toggle = function(node, link) { _queueAnim.push([node, link]); if (_isAnimating) { // nothing for now } else { var isOpen = Dom.hasClass(node, _CLASS_OPEN), end = isOpen ? 0 : Dom.getDimension(Dom.getFirstChild(node)).height, start = Dom.getDimension(node, true, true).height, o = {isOpen: isOpen, link: link, node: node, scope: _that}; _isAnimating = true; _that.onToggle.fire(o); Dom.animate(node, {height: {from: start, to: end}}, 0.5, YAHOO.util.Easing.easeBoth, [ {id: '_onComplete', 'fx': function() { _isAnimating = false; Dom.toggleClass(link, _CLASS_OPEN, ! isOpen); Dom.toggleClass(node, _CLASS_OPEN, ! isOpen); _queueAnim.shift(); _that._onToggle.fire(o); if (_queueAnim.length) {toggle.apply(this, _queueAnim.shift());} // activate next element in the queue }} ]); } }; // manage global blind animations _ED.register('click', {id: _COM_ANIM_BLIND, callback: function(e) { var targ = YUE.getTarget(e), node = Dom. exec (targ, targ.rel); YUE.stopEvent(e); if (node) { toggle(node, targ); } else { alert('JavaScript failure: issue with blind animation reference.'); } }}); // public namespace _F.prototype = { /** * A Custom Event object fired at the start of the toggle process. * @property onToggle * @static * @final * @type {Object} */ onToggle: new _CE(_COM_ANIM_BLIND + 'Toggle', null, true, YAHOO.util.CustomEvent.FLAT), /** * A Custom Event object fired at the end of the toggle process. * @property onToggle * @static * @final * @type {Object} */ _onToggle: new _CE(_COM_ANIM_BLIND + 'Toggle', null, true, YAHOO.util.CustomEvent.FLAT) }; _that = new _F(); return _that; })();

In addition to previously discussed features, the AnimBlindSingleton object has these features: a custom event fired before (’onToggle’) and after (’_onToggle’) the animation occurs, an animation queue, and will apply the class ‘open’. Advanced developers can subscribe to the custom events to execute a callback function when the animation starts or completes. An animation queue is used so that the user cannot trigger multiple blind animations at the same time, nor can they span a single trigger; this also prevents possible possible issues around animating again whilst in mid-animation. The ‘open’ class is applied to the trigger and the animation element so that designers can change styles when the blind is opened, such as changing the trigger text from ‘open’ to ‘close’.

So to recap, the benefits of using AnimBlindSingleton are that it is simple to use, requires no JavaScript, is completely standalone, and can be managed by the design. The biggest drawback is that since the singleton object doesn’t know anything about its triggers or animation elements, there is no easy way to programmatically trigger the blind animation. However, you can use a click simulator on the triggering anchors to programmatically fire the blind animation.

Lastly, I put together a demo page to show AnimBlindSingleton in action.

posted by Matt Snider at 11:21 am  

Saturday, January 3, 2009

Library - YUI 3 - Dynamic DOM Search Experiment

Today’s article illustrates an experimental function that parses a string, containing instructions for searching the DOM relative to a node. This method was inspired by a desire to allow a separate design team to specify the relative location of other DOM nodes. This way, parts of the JavaScript that handle user triggered events (such as ‘click’) need not hardcode relative nodes.

For example, suppose when the user clicks on an anchor, a relative node is made visible and the design team frequently changes the location of that node. An ID could be used, but that doesn’t work as well with dynamically generated content, instead the following method might be useful:

Example 1: Exec Function

YAHOO.util.Dom.exec = function(elem, instructions) { var node = YAHOO.util.Dom.get(elem); if (! (node && instructions)) {return;} var _s = instructions.split('.'); for (var i = 0; i < _s.length; i += 1) { if (node) { var task = _s[i]; if ($D[task]) { node = $D[task](node); } // todo: support childNodes[] else if (node[task]) { node = node[task]; } else { // unsupported technique } } else { return true; } } return node; };

The ‘exec’ method requires two parameters: the target node, and a string containing instructions for finding a node relative to the target. When both parameters are provided, the instructions string is split around period (’.'), creating an array of instructions to iterate on. YUI Dom methods and/or regular DOM methods may be used (as long as they don’t require brackets), such as the following: “parentNode.nextSibling.getFirstChild” (will execute the following “YAHOO.util.Dom.getFirstChild(node.parentNode.nextSibling)”). Lastly, ‘exec’ returns ‘undefined’ when a node is not found.

While, this method is useful, it is lacking a lot of features such as searching by class/tag name or children nodes. Libraries such as YUI 3 and jQuery have optimized DOM searching methods, which could be leveraged by this method. The reason I didn’t use these methods is that they only search down the DOM tree and not up, which I needed. Although there probably is an extension out there for this, but a quick search didn’t find it and what I have worked to meet my immediate needs. Next week, I will show a technique for dynamically attaching blind animations to the DOM that uses this method to relatively reference the node to animate from the triggering anchor.

posted by Matt Snider at 11:20 pm  
« Previous PageNext Page »

Powered by WordPress