Matt Snider JavaScript Resource

Understanding JavaScript and Frameworks

Tuesday, October 28, 2008

JQuery Like getElement Method in YUI

One of the many features that I like about jQuery is the ability to pass in CSS selectors and HTML attributes into the ‘jQuery’ for retrieving DOM nodes. YUI has methods for fetching elements by their ‘className’, but not by their HTML attributes, such as ‘type’ or ‘name’. Today, we’ll cover a method I wrote that supports fetching elements by tagName, className, and/or a collection of attributes. The method ‘getElements’ returns all nodes that match any of the tagNames and any of the classNames and every type of attribute (you can have multiple attribute types, such as ‘name’ and ‘type’) provided:

Example 1: getElements

Dom.getElements = function(tagNames, classNames, attributes, root) { var elems = [], nodes = []; // does the root exist, then use it if (root) { root = Dom.get(root); // if no root node, then no children; return empty array if (! root) {return [];} } // otherwise use document else { root = $doc; } // there are tag names provided if (tagNames && tagNames.length) { tagNames.batch(function(tag) { elems = elems.concat(Array.get(root.getElementsByTagName(tag))); }); } // use wildcard ‘*’ for tag name else { elems = Array.get(root.getElementsByTagName(’*')).slice(1); if (! elems.length && root.all) {elems = root.all;} // checking for IE < 6 } if (classNames || (attributes && attributes.length)) { var classFn = function(node) {return node;}, attrFn = function(node) {return node;}; // update the classFn to look at a list of classes if (classNames && classNames.length) { classFn = function(node) { return classNames.batch(function(klass) { if (Dom.hasClass(node, klass)) { return node; } }); }; } // update the attrFn to look at a list of attributes if (attributes && attributes.length) { var attributeMap = {}, attributeTypes = []; // iterate on the attributes and create iterables attributes.batch(function(attr) { if (! attributeMap[attr.attributeName]) { attributeMap[attr.attributeName] = []; attributeTypes.push(attr.attributeName); } attributeMap[attr.attributeName].push(attr.attributeValue); }); attrFn = function(node) { var isMatch = true; // iterate on the attribute types to find keys for the attribute value map attributeTypes.batch(function(attributeName) { // iterate on the values and compare with node var n = attributeMap[attributeName].batch(function(attributeValue) { if (attributeValue === node[attributeName]) {return node;} }); // node did not match, stop iterating, since this is not a match if (! n) { isMatch = false; return true; } }); return isMatch ? node : null; }; } // iterate on the nodes found by tags elems.batch(function(el) { if (classFn(el) && attrFn(el)) {nodes.push(el);} }); return nodes; } else { return elems; } };

First, I have shortened “YAHOO.util.Dom” to ‘Dom’ and this function required my Array.js to convert DOM node collections into arrays. The parameters for ‘getElemetns’ are four optional values: an array of tag name strings, an array or class name strings, an array of attribute objects {attributeName, attributeValue}, and the root node to search the DOM from. If the root node is not provided, then ‘getElements’ will start searching from the document, but if an invalid root node is provided, then ‘getElements’ will return an empty array. If tag names are provided, then ‘getElements’ concatenates the results of each into an array (’elems’), but will otherwise default to wildcard (’*'), retrieving all sub-nodes.

When class names or attributes are provided, ‘getElements’ will iterate on the ‘elems’ array and create a new array ‘nodes’ containing the elements that matched the class and attribute values. However, if neither is provided, we simply return the ‘elems’ array. To determine if the classes and/or attributes are present on a node, we create functions that take a node as a parameter and return that node when the value matches. The class name function will test to see if the node contains any of the classes in the array. The attribute function first creates a map of values for each type of attribute that is provided, then iterates through them, only returning true when the node matches at least 1 attribute of each type.

Here is an example that starts searching for ‘input’ and ‘a’ tags, with either class ‘button’ or ‘link’, and has the name ’submit’:

Example 2: Using ‘getElements’

Dom.getElements([’input’, ‘a’], [’button’, ‘link’], [{attributeName: ‘name’, attributeValue: ’submit’}], ‘myRootNode’);
posted by Matt Snider at 10:48 am  

Sunday, October 26, 2008

Regex Issues with $0-9 In Replacement Text

On a financial site, such as Mint.com, there are many opportunities to use regular expressions to replace numbers and currency. While replacing numbers with regular expressions will work fine, replacing US currency will cause issues that you should be aware of. The issues arise when using the “String.prototype.replace” method, because a dollar sign (’$') followed by a number is a back-reference to a captured parenthesis in the regular expression. Meaning that when the replacement string contains ‘$1′, the ‘$1′ will actually be a pointer to the content matched by the first parenthesis in the regular expression, instead of the string ‘$1′. Consequently, replacing some text with ‘$19.99′, might not actually replace the text as intended. Here is an example:

Example 1: Problem with Replace and $19.99

var haystack = ‘My budget is INSERT_AMOUNT.’; var result = haystack.replace(/(INSERT_AMOUNT)/, ‘$19.99′);

You might expect ‘result’ in Example 1 to equal, “My budget is $19.99″, however it is actually going to equal, “My budget is INSERT_AMOUNT9.99″. The ‘$1′ back-references to the first parenthesis in the regular expression, which was ‘INSERT_AMOUNT’. You can prevent this by using the double ‘$’ escape, writing the ‘$1.00′ as ‘$$1.00.

Alternatively, if your regular expression does not contain any capturing parenthesis, then you will not have to worry about accidentally back-referencing values, except for one exception with ‘$0′ in Safari. This special-case results from the fact that, in addition to the dollar sign and 1-9 back-referencing, there is also a special back-reference ‘$0′ which returns the substring of ‘haystack’ matching your whole regular expression (not just the values captured by parenthesis). For example:

Example 2: The ‘$0′ Back-Reference

var haystack = ‘My budget is INSERT_AMOUNT for MONTH_NAME.’; var result = haystack.replace(/INSERT_AMOUNT.*?MONTH_NAME/, ‘$0.99 for February’); var result_zero_ref = haystack.replace(/(INSERT_AMOUNT)(.*?)(MONTH_NAME)/, ‘$0.99′);

The ‘result’ variable in Example 2 will be set to “My budget is $0.99 for February” in all browsers except Safari, while ‘result_zero_ref’ will be set to “My budget is INSERT_AMOUNT for MONTH_NAME.99″, because we are using capturing parenthesis and the replacement is being replace with a pointer to itself plus the text ‘.99′. In Safari, ‘result’ will also behave like ‘result_zero_ref’ and be set to “My budget is INSERT_AMOUNT for MONTH_NAME.99 for February”, even though there are no capturing parenthesis. Although, most browsers do not populate the ‘$0′ back-reference if there are no capturing parenthesis, Safari always populates it. Fortunately, Safari also supports dollar sign escaping ‘$$’ in the replacement text, so you can us that to prevent ‘$0′ from back-referencing.

———

There are actually 4 techniques you could use to prevent this problem. The double dollar escape is probably the best, but here are the other technique: 1) avoid putting ‘$’ in the replacement text, except for back-references, which can be done by crafting more complex regular expressions that use back-references to populate the dollar sign; 2) use an intermediary step that strips out all dollar signs from the replacement text, before replacing, then inserts them back in; 3) use the double dollar escape; or 4) use a function callback.

The first method is great, if you have no need to change the currency symbol. The dollar escape is still the best method, but before I knew about it, I used to prefer the second method. The second method is more robust than the first, allowing you to switch currencies. Here is my implementation:

Example 3: replaceCurrency

String.prototype.replaceCurrency = function(rx, s) { return (s && rx) ? this.replace(rx, s.replace(/$/g, ‘#_#’)).replace(/#_#/g, ‘$’) : ” + this; };

In Example 3, we first globally replace all the ‘$’ in the replacement text ’s’ with ‘#_#’, then perform the regular expression. Once the expression completes we go ahead and replace all the ‘#_#’ with ‘$’, thereby restoring the dollar signs. I choose to use ‘#_#’, because it contains no regular expression special characters and is a string of characters that does not occur in natural language. This technique requires 3 regular expression, instead of 1, but works in all browsers. The biggest draw-back to this method and the double dollar escape is that if you escape all ‘$’ in your replacement text, then you cannot mix currency with back-references in your replacement text. If you need something more effective than Example 3, you might want to check out Steven Levithan’s regex library.

Lastly, here is demo page that illustrates the issues expressed in Example 2.

posted by Matt Snider at 9:21 pm  

Wednesday, October 22, 2008

Animation Comparison of 3 Top Libraries

There are many conversations between designers and engineers that go back and forth about the performance of animations in JavaScript. Often there is an expectation of animations to run consistently (same speed, same step size, same in all browsers, etc.), and as awesome as that would be, this pushes at the limitations of JavaScript.

Many designers experience a great working animation on a demo page, and in turn, expect that the animation can be plugged into a complex JavaScript library, whilst preserving its performance.

Again, as wonderful as this would be, there are a few things that need further explanation. Today’s article will look at a simple animation in 3 top libraries: YUI, jQuery, and Prototype/Scriptaculous; and show how uniformly all the animations work well on a simple page, but degrade as the page becomes busier.

Many of you probably know that there are a number of issues affecting how well an animation works in JavaScript: how busy the client machine is, how much CPU/memory your browser dedicates to JavaScript, how much JavaScript is needed to run a given page, and how much work the JavaScript is already doing; to name a few. Also, animation libraries use a combination of the ’setTimeout’ or ’setInterval’ methods of JavaScript, which are notoriously inaccurate (+/- 15ms), and/or (new Date()).milliseconds() to attempt to smooth animation, but it is fairly easy for an entire step in the animation to be skipped. The summary is that the more complex a page gets the less consistent that animations become.

I choose to compare these three libraries because: YUI is my library of choice, as it was designed by engineers for engineers; Prototype/Scriptaculous is probably the most used library (thanks to ruby on rails and cakePHP); and jQuery is the most popular designer-used library. However, I expect that GWT, Mootools, Dojo, Mochikit, and all the others to behave about the same.

For this experiment we are building 3 animations that increase the width of an element from 100px to 400px, and then the reverse, using each library independently. Fortunately, both YUI and jQuery use self-contained namespaces, so they don’t conflict with Prototype. Here is the code required for each:

Example 1: jQuery

var isOpenedJquery = false, isJQueryAnimated = false; var jQueryCallback = function() { if (! isJQueryAnimated) { isJQueryAnimated = true; isOpenedJquery = ! isOpenedJquery; jQuery(’#containerAnimateJQuery’).animate({width: isOpenedJquery ? ‘400px’ : ‘100px’}, 1000 * globalDuration, null, function() {isJQueryAnimated = false;}); } }; jQuery(’#triggerAnimateJQuery’).click(jQueryCallback);

Example 2: Prototype

var isOpenedPrototype = false, isAnimatedPrototype = false; var prototypeCallback = function() { if (! isAnimatedPrototype) { isOpenedPrototype = ! isOpenedPrototype; isAnimatedPrototype = true; Effect.BlindDown(’containerAnimatePrototype’, {duration: globalDuration, scaleY: false, scaleX: true, scaleFrom: isOpenedPrototype ? 100 : 400, scaleTo: isOpenedPrototype ? 400 : 100, afterFinish: function() {isAnimatedPrototype = false;}, afterUpdate: function() {YAHOO.util.Dom.setStyle(’containerAnimatePrototype’, ‘height’, ‘20em’);}}); } }; $(’triggerAnimatePrototype’).observe(’click’, prototypeCallback);

Example 3: YUI

var isOpenedYui = false; var containerAnimate = { inYUI: new YAHOO.util.Anim(’containerAnimateYUI’, {width: {to: 100}}, globalDuration), outYUI: new YAHOO.util.Anim(’containerAnimateYUI’, {width: {to: 400}}, globalDuration) }; var yuiCallback = function() { if (! (containerAnimate.outYUI.isAnimated() || containerAnimate.inYUI.isAnimated())) { isOpenedYui = ! isOpenedYui; containerAnimate[isOpenedYui ? ‘outYUI’ : ‘inYUI’].animate(); } }; YAHOO.util.Event.on(’triggerAnimateYUI’, ‘click’, yuiCallback);

All implementations use a ‘click’ event callback on a button to trigger animating. There is a global ‘globalDuration’ variable that can be adjusted on the demo page; YUI and Prototype/Scriptaculous use seconds, while jQuery uses milliseconds. The callback function first checks to ensure that we are not already animating, which is done by setting a boolean via a callback for Prototype/Scriptaculous and jQuery, and calling the ‘isAnimated’ method on the Anim object in YUI. Then we use a ‘isOpened’ boolean to determine whether to animate in or out. In Prototype/Scriptaculous and jQuery we call 1-off methods that runs the animation, whereas in YUI we create ‘Anim’ objects and call the ‘animate’ methods.

Take a look at the demo, here.

What I Learned

  • I still don’t like Prototype/Scriptaculous. It took me forever to get the animation working (hours, compared to minutes with the other libraries). *Thanks to Artyom prototype is now working correctly.
  • Prototype/Scriptaculous and jQuery use a linear progression by default, while YUI uses uniform steps by default.
  • All 3 libraries have a variety of easing methods to accelerate/decelerate the step size
  • They all work great under no-load, and fairly well under load. They occasionally glitch, but tend to be rather minor.
  • Matt Sweeney, of YUI, has provided a sinoidal easing method for us, so that YUI animates about the same as the other libraries.
posted by Matt Snider at 10:39 am  

Tuesday, October 14, 2008

IE Issues to Avoid

As many of you know, Mint.com has just come out of beta with our new release (v8). During this last development cycle we revamped our transaction page to have an inline edit, instead of using div-based popup dialogs. This has been my baby for the past two months, and was a lot of work. During the design and development process most browsers behaved very well, but IE really gave me some problems. Today, I want to discuss those issues, so that you can avoid them in your own projects.

Opacity Crop:

If you have an element in the DOM with children element(s) positioned outside of the parent’s dimensions, say with “position:relative” or using margins, and then apply an opacity to the parent element, IE will crop all elements positioned outside of the parent. This happens because IE uses a special Microsoft filter to render the opacity, which redraws the parent element and its children to the right opacity, but only considers the dimensions inside the parent element for this redraw. Thus, everything positioned outside of the parents dimensions are lost.

Image 1: Normal Popup

before opacity is applied

Image 2: Cropped Popup

after opacity is applied

Opacity Breaks Background Alpha Opacity:

In addition to the above issue, applying an opacity in IE also breaks any alpha transparency background images use by any element inheriting from the element where the opacity is applied. In Image 1, you see the nice alpha transparency background used in the popup footer to apply a shadow. Then in Image 2, after opacity has been applied, you see that alpha transparency is now black.

The way to prevent both these issue is to not apply an opacity to any ancestor of an element positioned outside of the ancestor and/or with a background alpha transparency. This includes animations that use opacity, because once the opacity is applied, the cropping cannot be removed. Even setting the opacity back to 100 will not restore the original settings, because it still uses the Microsoft opacity filter.

Percentage Width on Inputs:

The last issue I ran into was with input elements inside of a table. On Mint we use a background image, on a table cell in the edit row to provide the look and feel for the input, while the actual input element is transparent with a 95% width. This works great in all browser, except in IE when the length of the value in the input is greater than the 95% width. In that case, the input and TD tags resize to fit the text. The solution is to use fixed width for IE, because IE resizes elements width percentage width, if the content is larger than the width.

Here is a test page so you can experience these lovely IE issues for yourself.

posted by Matt Snider at 12:26 pm  

Friday, October 10, 2008

The New IE PNG Fix

I have been using the iepngfix.htc tool for years, allowing me to correct IE6’s lack of support for alpha transparencies in PNGs. The tool is called via a CSS behavior, which loads the HTC file containing JavaScript that searches the CSS for PNG backgrounds and automatically applies the IE6 transparency filter. So once the page loads, the PNG background images are corrected. There is a slight flicker as the correct happens, but it is either that or maintain two sets of images, one for every other browser, and one for IE6. Frankly, I choose to optimize for the 90% of users.

However, this tool has always had two major drawbacks: 1) when the HTC file works its onload magic, all background positioning of your transparent PNGs is ignored; and 2) any content dynamically added to the DOM will not have the filter applied. Personally, I found the 2nd to be more of an issue, but the 1st problem prevents designers from spriting their graphics.

Fortunately, Angus Turnbull of TwinHelix, author of iepngfix, recently (v2.0 alpha 3) fixed the shortcomings of the HTC file, by adding an additional JavaScript file to be included as well. This JavaScript file includes an “IEPNGFix.update” function that corrects the background position of PNGs and it can be used to apply the filtering to dynamically added DOM content. Natively, “IEPNGFix.update” is only called on a window resize, however, once you have included it in your project, you can call it whenever you need to.

Example 1: Calling IEPNGFIX.update

if (window.IEPNGFix && IEPNGFix.update) {IEPNGFix.update();}

Use the code snippet from Example 1 anytime you have added elements to the DOM with a transparent PNG background and/or you have adjusted the background position of any transparent PNG. Now, even in IE6, you can have your have your cake and eat it too.

Below are some additional examples on how to include the HTC and JavaScript files in your code:

Example 2: HTML > Head

<!–[if lt IE 8]> <link rel=”stylesheet” type=”text/css” href=”sc/dev000/css/iehacks.css” /> <script type=”text/javascript” src=”sc/dev000/js/lib/iepngfix_tilebg.js”></script> <![endif]–>

Example 2 will include your IE hack CSS and the iepngfix_tilebg JavaScript anytime it encounter a version of IE that is less than 8. You could adjust this to be less than 7, if you only want to target IE 6 and lower.

Example 3: Behavior CSS

* html .iepng { behavior: url(pathFromHTMLFileToHtc/iepngfix.htc); }

If you include Example 3 in your IE hacks CSS, then it will apply the IE PNG fix logic to any element with the className ‘iepng’. You can add any specificity you want, but I prefer to explicitly determine what elements have this behavior applied to them. Lastly, the path to your HTC file should be from the directory that serves the HTML file, not the directory of the CSS file, because that is how IE transverses directories when applying filters.

posted by Matt Snider at 10:51 am  

Wednesday, October 8, 2008

Scalable DOM Module With Rounded Corners

I am working on a homepage for this site and several other site improvements. For that work, I wanted a reusable DOM fragment with accompanying CSS that had rounded corners, a slight gradient background, and scaled well. Here is an example of what I designed. The HTML markup is fairly simple:

Example 1: Module HTML Markup

<div class=”module”> <div class=”moduleHeaderOuter”> <div class=”moduleHeaderInner”><h3>Header Content Here</h3></div> </div> <div class=”moduleContentOuter”> <div class=”moduleContentInner”>Content Here</div> </div> <div class=”moduleFooterOuter”><div class=”moduleFooterInner”></div></div> </div>

The “div.module” element is just a container and can be styled the way you like it, although it will need to be at least as wide as the graphics you use for the right-side of the backgrounds (more on this later). I chose to use 6 divs to manage the background in a scalable, x-browser way. The outer divs render the right side of the background, while the inner divs contain content and render the left side of the content. The background sides have to be separated for two reason: we do not want to use a fixed width for the module content, so the CSS and background images need to be size naive; and the rounded corners backgrounds cannot overlap, or the solid background of one will cover the transparent part of the other.

You can add content to the inner header, which I have done with a sectional ‘h3′ tag, and to the inner content. The inner content has no height constraints as we are repeating an image with a 1px height, but the header row should only have one line height. I used image spriting for the backgrounds and the header only has 60px of background height, which should be reserved for user-driven font scaling. The footer is only an image of 10px height, so it is currently designed as a placeholder and cannot contain any content (this is to reduce the image size of the sprite). Originally, I used the ‘h3′ tag instead of the inner and outer header divs, but in the end I decided to use extra divs because the ‘h3′ tag has differing font-sizes from the other wrapper divs, and this way is easier to read and more consistent.

For the background to work, I need three image files: a sprite image for the header and footer (4 images in this sprite, one for each corner), a 1px height image for the left background, and a 1px height image for the right background. The left-side images need to be 2x wider than the intended width of your widest module widget, so that they can scale, while the right-side images need only be 1/4 the width of the left-side images, as they do not need to scale very much. The height of the header sprite is 3x greater than the intended height of the headers in the example, to allow for scaling. If you decide that you want to put content into the footers, simply increase the height of the footer sprites to a value 3x greater than the default height of your footer.

So now that we have the images and the HTML, here is the CSS that puts it all together:

Example 2: Module CSS

div.moduleFooterOuter, div.moduleContentOuter, div.moduleHeaderOuter { margin-left: 1em; margin-right: 1em; padding-right: 1em; } div.moduleFooterInner, div.moduleContentInner, div.moduleHeaderInner { padding: 0em 1em; /* this spaces the content away from the module edge */ } div.module div.moduleHeaderOuter { background: transparent url(../images/sprite/module_sprite.gif) no-repeat right -120px; } div.module div.moduleHeaderInner { background: transparent url(../images/sprite/module_sprite.gif) no-repeat left top; padding: 0.2em 1em; } div.moduleFooterOuter { background: transparent url(../images/sprite/module_sprite.gif) no-repeat right -360px; } div.moduleFooterInner { background: transparent url(../images/sprite/module_sprite.gif) no-repeat left -240px; height: 11px; } div.moduleContentOuter { background: transparent url(../images/bg/module_bg_right.gif) repeat-y right top; } div.moduleContentInner { background: transparent url(../images/bg/module_bg_left.gif) repeat-y left top; }

The outer containers use margins, because the “div.module” class will most likely have a width applied, and IE 6 behaves poorly when you apply a width or height and a margin or padding to an element. The “padding-right” of the outer container is what prevents the background images from overlapping each other. The only other tricky part is the negative positions on some of the backgrounds, which are used to find the right sprite as they are positioned every 120px in ‘module_sprite.gif’.

See it all working together in this Module Example.

posted by Matt Snider at 10:14 am  

Sunday, October 5, 2008

Robust Get Element Dimension Function

As mentioned earlier this week, today we will be discussing a ‘getDimension’ function that corrects for certain styles (”display:none” and “overflow:hidden”) that cause an element to not have the proper height/width. This method contains generic logic that will determine if the element is “display:none” or “overflow:hidden”, whether styled inline or via CSS (thanks to YUI), and temporarily correct those styles to determine the actual height/width of the element. It works with any element (not just block level), will restore any styles that it changes, and allows you to override the default correction behavior.

Example 1: getDimension

Dom.getDimension = function(elem, ignoreDisplay, ignoreOverflow) { var node = Dom.get(elem), x = 0, y = 0, width = 0, height = 0; // node exists if (node) { var stl = node.style, originalDisplay = Dom.getStyle(node, ‘display’), originalOverflow = Dom.getStyle(node, ‘overflow’), originalPosition = Dom.getStyle(node, ‘position’), originalVisibility = Dom.getStyle(node, ‘visibility’), originalHeight = node.offsetHeight, correctDisplay = ‘none’ === originalDisplay && ! ignoreDisplay, correctOverflow = ‘hidden’ === originalOverflow && ! ignoreOverflow; // not displayed; temporarily display if (correctDisplay) { stl.visibility = ‘hidden’; stl.position = ‘absolute’; stl.display = ‘block’; } // overflow hidden; attempt to correct if (correctOverflow) { stl.visibility = ‘hidden’; stl.position = ‘absolute’; stl.height = ‘auto’; } // determine dimensions var region = Dom.getRegion(node); x = region.left; y = right.top; width = region.right - x; height = region.bottom - y; // restore overflow if (correctOverflow) { stl.height = originalHeight + ‘px’; stl.visibility = originalVisibility; stl.position = originalPosition; } // restore corrected display if (correctDisplay) { stl.visibility = originalVisibility; stl.position = originalPosition; stl.display = originalDisplay; } } return {x: x, y: y, height: height, width: width}; };

*Note: I assume that you are using YUI’s “yahoo-dom-event.js” and that you have a shortcut “Dom” that points to ‘YAHOO.util.Dom”.

First we ensure that the element is an Element object, not an ID string, and initialize default value of ZERO for each dimension. When the node exists in the DOM we fetch the required styles using the YUI “Dom.getStyle” method, which corrects for styles applied via CSS, and determine if we need to correct for “display:none” or “overflow:hidden”. You can prevent these corrections if you set either ‘ignoreDisplay’ or ‘ignoreOverflow’ to true. If we need to correct the display, we style the visibility to ‘hidden’, so it won’t appear when we style the element “display:block”, and ‘position’ it ‘absolute’ so that it won’t move other elements around in the DOM. If we need to correct for the overflow, we do the same, but instead of applying “display:block”, we set the ‘height’ to ‘auto’, which will cause the element height to reflect its contents. Then we determine the dimensions using the YUI “Dom.getRegion” method, which returns the ‘top’, ‘right’, ‘bottom’, and ‘left’ values of the element. Lastly, before returning the dimensions, we restore any ‘height’, ‘visibility’, ‘position’, and/or ‘display’ styles to their original values.

This method has fairly simple logic, but is immensely helpful if you need the full dimension of elements that are not or might not be displayed. I put together a test page, so that you can see ‘getDimension’ in action.

posted by Matt Snider at 2:24 pm  

Wednesday, October 1, 2008

Blind Up/Down Using YUI Animation

The Prototype Library extension, Scriptaculous has some really nice animation widgets, but it is built on a library that I do not like using. My favorite animations from scriptaculous are ‘blindUp’ and ‘blindDown’. Today we will be using the YUI animation package to write a widget that simplified writing ‘blind’ animations. The goal is to take a DOM node and instantiate a widget that manages scrolling the height of the DOM node up/down.

Here is my first pass at a blind up/down animation widget:

Example 1: BlindAnimator Widget

Core.Widget.BlindAnimator = function(id, cfg) { // local namespace var Anim = YAHOO.util.Anim, config = Object.is(cfg) ? cfg : {}, Dom = YAHOO.util.Dom, F = function() {}, isDown = false, that = null; // DOM namespace var dom = { node : Dom.get(id) }; if (! dom.node) {return;} // node is required // update config if (! config.animSpeedDown) {config.animSpeedDown = 0.5;} if (! config.animSpeedUp) {config.animSpeedUp = 0.5;} if (! config.bottomPadding) {config.bottomPadding = 0;} if (! config.minHeight) {config.minHeight = 0;} if (! config.maxHeight) { var r = Dom.getRegion(dom.node); config.maxHeight = r.bottom - r.top; isDown = true; } // initialize animations var anim = { blindDown: new Anim(dom.node, {height: {from: config.minHeight, to: config.maxHeight}}, config.animSpeedDown, config.easing), blindUp: new Anim(dom.node, {height: {from: config.maxHeight, to: config.minHeight}}, config.animSpeedUp, config.easing) }; // setup animations anim.blindDown.onStart.subscribe(function() {Dom.setStyle(dom.node, ‘overflow’, ‘hidden’);}); anim.blindUp.onComplete.subscribe(function() {Dom.setStyle(dom.node, ‘overflow’, ‘visible’);}); anim.blindDown._onComplete.subscribe(function() {isDown = true;}); anim.blindUp._onComplete.subscribe(function() {isDown = false;}); // scrolls the page as the bind opens if (config.scrollToViewport) { anim.blindDown._onTween.subscribe(function() { // handles scrolling the window to fit during animation var dim = Dom.getRegion(dom.node), offset = Core.Client.getScrollOffset(), view = Core.Client.getViewportSize(), pos = dim.bottom + config.bottomPadding, bottom = offset.y + view.y; if (pos > bottom) { window.scroll(0, offset.y + pos - bottom); } }); } // public namespace F.prototype = { blindDown: function() { that.stop(false); anim.blindDown.animate(); }, blindUp: function() { that.stop(true); anim.blindUp.animate(); }, stop: function(isDown) { anim.blindDown.stop(isDown); anim.blindUp.stop(! isDown); }, toggle: function() { that[isDown ? ‘blindUp’ : ‘blindDown’](); }, subscribe: function(name, fn, isUp, o) { var a = isUp ? anim.blindUp : anim.blindDown; if (a[name] && a[name].subscribe) { a[name].subscribe(fn, o); } } }; that = new F(); return that; };

BlindAnimation.js

Instantiate a new BlindAnimator by passing it the ID of the DOM node you wish to animation; you may also provide a configuration object. If no configuration is passed, the widget assumes that the DOM node is currently in the blindDown (opened) state and that ZERO is the blindUp (closed) state. There are currently 7 configuration objects:

Example 2: Configuration

{ animSpeedDown: a number where 1 represents 1000ms (default is 0.5), animSpeedUp: a number where 1 represents 1000ms (default is 0.5), bottomPadding: the bottom margin when using scrollToViewport (default is ZERO), minHeight: the blindUp height (default is ZERO), maxHeight: the blindDown height (default is node height), scrollToViewport: true|false, when true will scroll the page to fit blindDown animation (default is false), easing: YAHOO.util.Easing.EASING_TYPE (default is none) }

Each instance of BlindAnimator has 5 public methods: ‘blindUp’, ‘blindDown’, ’stop’, ‘toggle’, and ’subscribe’. The ‘blindUp’ and ‘blindDown’ methods provide public access to the animations, while also ensuring that any previous animation has stopped, calling the ’stop’ method that terminates both animations. The ‘toggle’ method calls the ‘blindUp’ or ‘blindDown’ method, with the opposite state that the widget is not currently in. And lastly, the ’subscribe’ method exposes the Custom Events that are attached to the animations, allowing additional customization.

The key to any blind animation is the proper application of ‘overflow’ style. As you can see, we toggle the ‘overflow’ to ‘hidden’ just before we scroll down, which hides the content of the DOM node outside of the node’s height. As the height increases we get the blind opening effect. The same is true, when we decrease the height and we get the blind close effect. After the ‘blindUp’ animates, we then reset the ‘overflow’ to ‘visible’.

So far I am pleased with this widget, but I see three areas for improvement: 1) when frequently triggering an event that calls ‘blindUp’ or ‘blindDown’, the animation jumps around a bit; 2) YUI does not attempt to determine dimensions of elements that are initially displayed ‘none’; and 3) I can foresee situations when min/max height need to be dynamic. The first issue can be addressed by better handling of the ‘isDown’ state when animations start and stop. The second issue can be solved by a method that temporarily displays the element off the screen, while we attempt to determine its dimensions (to be discussed in our next article). The third issue is tricky and will most likely involve additional configuration properties.

Here is a test page, where you can play around with BlindAnimator.

posted by Matt Snider at 12:16 am  

Powered by WordPress