Matt Snider JavaScript Resource

Understanding JavaScript and Frameworks

Tuesday, November 25, 2008

Simulating Events Using YUI

Sometimes when managing pages with unobtrusive JavaScript, especially ones leveraging events that need to bubble, two code paths are required: one for the event handler that properly bubbles, and another for the same action triggered by code. For example, a ‘click’ event is attached to a ‘div’, containing many children, including several ‘input’ elements. If the client clicks on one of the inputs, then both the ‘input’ and the ‘div’ ‘click’ events fire. However, if you programmatically focus on the element neither event fires, even though it might be desired.

Some browsers have begun to support event emulation allowing engineers to trigger client events, but most are incomplete and there is no standard x-browser way to simulate events. Fortunately, JavaScript Frameworks have come to the rescue, allowing us to simulate client events. YUI has the YUI Test utility, which allows for the emulation of 7 mouse events and 3 keyboard events: click, dblclick, mousemove, mousedown, mouseup, mouseover, mouseout, keydown, keyup, and keypress. So by using YUI there actually is a standard, x-browser way to emulate events.

Most JavaScript Frameworks have an x-browser event handling system, and many keep lists of all the events that you have attached using their utilities. By iterating on the list of cached event handlers, libraries can properly bubble (or capture) events starting from a DOM node and moving up the node tree. Here is a simple example of a click simulator that I wrote using YUI, illustrating how the Test library can leverage the Event utility to simulate events:

Example 1: Click Handler Function

var simulateClickEvent = function(elem) { var node = YAHOO.util.Dom.get(elem); while (node && window !== node) { var listeners = YAHOO.util.Event.getListeners(node, 'click'); if (listeners && listeners.length) { listeners.batch(function(o) { o.fn.call(o.adjust ? o.scope : this, {target: node}, o.obj); }); } node = node.parentNode; } };

To use ’simulateClickEvent’, simply pass in the ID or pointer to a DOM element. The method tests if the ‘node’ exists and that it is not equal to the ‘window’ before continuing its operation. At the end of the ‘while’ statement, it moves to the parent node of ‘node’, and continues that way up the DOM tree until window is reached or ‘null’ if the node was not properly attached to the DOM. Inside the loop the ‘getListeners’ method of the ‘Event’ utility is called, passing in the current node pointer and the desired event (in this case ‘click’). If nothing is found for the event/node combination, then YUI will return ‘null’, otherwise an array of events objects. Then to simulate events, the listeners array is iterated on and the function attached to the event object is called. In this example, a simple Event Object is created, with only the ‘target’ key set.

The YUI Test infrastructure is much more robust than my simple example, allowing engineers to specify many more properties of the event that will be passed into the callbacks. However, most of the time you probably won’t need that much power and something more rudimentary is acceptable, especially if you do not want to include another JavaScript file into your library. In those cases, I think my little example, with little modifications for your specific requirements, will probably suffice.

posted by Matt Snider at 9:47 am  

Wednesday, November 19, 2008

Element Editor

About a week ago, Nik, posed a question about how to record edits in his data table widget. Responding to his inquiry, inspired todays article, which covers the “Element Editor” object (a work in progress). The ultimate goal of the project is to develop a widget that can be used to dynamically update the content of any DOM node, by inserting an editable form field (input, textarea, select, etc.). In version 1, “Element Editor” can replace a DOM node with a textarea or an input, allowing a client to replace the text inside of the DOM node. The widget contains a singleton Custom Event (1 Custom Event shared by all ‘ElementEditor’ instances), which can be subscribed to, that fires each time a user changes the DOM node content.

Example 1: Element Editor

Core.Widget.ElementEditorEvents = new YAHOO.util.EventProvider(); Core.Widget.ElementEditor = function(elem, conf) { var YE = YAHOO.util.Event, YD = YAHOO.util.Dom, YK = YAHOO.util.KeyListener.KEY, CE = Core.Widget.ElementEditorEvents; YE.off = YE.removeListener; var cfg = conf || {}, F = function() {}, node = YD.get(elem), text = '', that = null; // configure if (! cfg.tagName) {cfg.tagName = 'textarea';} if (! cfg.type) {cfg.type = 'text';} if (! cfg.cols) {cfg.cols = 5;} if (! cfg.rows) {cfg.rows = 5;} if (! cfg.name) {cfg.name = '';} // event namespace var E = { onClick: function(e) { text = node.innerHTML; YE.stopPropagation(e); var dim = YD.getRegion(node), child = document.createElement(cfg.tagName); node.innerHTML = ''; if (cfg.fitToParent) { YD.setStyle(child, 'height', dim.bottom - dim.top + 'px'); YD.setStyle(child, 'width', dim.right - dim.left + 'px'); } node.appendChild(child); child.value = text; if (YAHOO.lang.isFunction(child.focus)) {child.focus();} // focus on the new element child.name = cfg.name; // special-case for each tag name switch (cfg.tagName) { case 'textarea': child.cols = cfg.cols; child.rows = cfg.rows; break; case 'input': child.type = cfg.type; if (! cfg.size) {child.size = cfg.size;} if (! cfg.maxLength) {child.setAttribute('maxLength', cfg.maxLength);} break; default: } YE.off(node, 'click', E.onClick); YE.on(child, 'blur', E.onBlur); YE.on(node, 'keydown', E.onKeyDispatcher); }, onBlur: function(e) { var s = node.firstChild.value; YE.stopPropagation(e); YE.on(node, 'click', E.onClick); YE.off(node, 'keydown', E.onKeyDispatcher); YE.off(node.firstChild, 'blur', E.onBlur); node.innerHTML = s; if (text !== s) {CE.fireEvent(that.CE_CHANGE, {id: cfg.id, element: node, value: s});} }, onKeyDispatcher: function(e) { switch (YE.getCharCode(e)) { case YK.ENTER: YE.stopEvent(e); E.onBlur(e); break; case YK.ESCAPE: node.firstChild.value = text; E.onBlur(e); break; default: } } }; YE.on(node, 'click', E.onClick); // public namespace F.prototype = { CE_CHANGE: 'elementEditorSave', subscribe: function(p_type, p_fn, p_obj, p_override) { CE.subscribe(p_type, p_fn, p_obj, p_override); } }; that = new F(); // lazily inialize custom events if (! CE.hasEvent(that.CE_CHANGE)) { CE.createEvent(that.CE_CHANGE); } return that; };

First, the singleton event provider is initialized. In your own projects, if you already use a global event provider, you can just hook into it. The ‘ElementEditor’ object is instantiated by passing a reference to the DOM node and an optional configuration parameter. When initializing, the object first creates local pointers to YUI, initializes some internal variables, then defines default configuration. The configuration is still basic, allowing the engineer to specify several attributes of the editable field created by the widget, but will become more complex and less restrictive in future versions. Next the event callbacks are defined, the ‘click’ event is subscribed too, and a public ’subscribe’ method is defined to expose the Custom Event. Lastly, if the Custom Event singleton is not already defined, we go ahead and initialize it.

All the magic begins, when the client clicks on the node that was passed in to initialize the ‘ElementEditor’. After clicking: the content of the node is stored into a ‘text’ variable, the editable field replaces all children of the node, and the ‘click’ event is unsubscribed whilst ‘blur’ and ‘keydown’ events are subscribed to. If the ‘fitToParent’ parent configuration is set, then the new edit element will be sized to fill the parent node. Also, if the native JavaScript ‘focus’ function is available on the newly created field (textareas and inputs), then we focus on the field as well.

The ‘keydown’ callback listens for the escape key and the enter key, dispatching the right actions accordingly. Escape will restore the previous content and ignore any changes that the user has made. Whilst the enter key will forward to the ‘blur’ event handler. The ‘blur’ callback unsubscribes the ‘keydown’ and ‘blur’ events, before resubscribing the ‘click’ event. It also, replaces content of the node with the client generated value in the editable field. Lastly, it will fire the save Custom Event (’CE_CHANGE’) if the client has made any changes. Any callback methods subscribing to ‘CE_CHANGE’ will be passed an object as the first (and only) parameter, containing the keys: ‘id’, ‘element’, ‘value’, where ‘id’ is a unique key defined by the object configuration, ‘element’ is the node, and ‘value’ is the client generated content.

This widget can be used with a table cell in a data grid, but the goal is to allow the editing of any DOM node. Here is a couple of examples of how to use “Element Editor”.

posted by Matt Snider at 11:38 am  

Wednesday, November 12, 2008

Node Tree Visualization

Originally, this post was intended to be a response to sam’s question about an efficient way to store node trees. While, he found the answer to his question, I wanted to compare the searching efficiency for a few different methods for storing node trees. However, I was having too much fun working on the node tree visualization algorithm, so I want to share it today, and I will get to the tree node searching efficiency in another post.

The visualizer draws a DIV for each node in the tree, where the width of that DIV is the accumulated with of all its children and there children. By increasing the width of a parent node to umbrella its children, it is very easy to understand the tree hierarchy. In addition, each leaf node is colored green. The trickiest part was managing the spacing of nodes, when there were previously leaf nodes at a lower depth.

The code is really messy, so we won’t go into the details. Basically, we start with the root and traverse down the left-side of the tree, first creating the div representation of a node, then its children from left to right. Each depth creates a div containing all the nodes for that depth, and each node floats left. While traversing the node tree, the number of total children nodes bubbles up the tree, which is used to determine the width of parent nodes. If there are any leafs nodes for a depth, they are added to a list, and a spacer for each will be added the next time a lower depth is reached. A correction list is used to monitor whether we have already corrected for leafs at a given depth or not.

Putting it all together we have a node tree visualizer demo, which generates a random small node tree.

posted by Matt Snider at 11:27 pm  

Sunday, November 9, 2008

Unique ID Generator

In preparation for an article on building and traversing node trees, I realized that I had not yet written an article covering a method to generate unique IDs, which is needed to provide a unique identifier for each node. We need a method that generates a unique string and does not repeat any ids. In addition, there should be an optional parameter to check that the ID does not yet exist in the DOM. Here is the method:

Example 1: Unique ID Generator

var MES_UNIQUE_IDS = {}; var mes_generateUniqueId = function(charset, len, isNotInDOM) { var i = 0; if (! charset) {charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';} if (! len) {len = 8;} var id = '', charsetlen = charset.length, charIndex; // iterate on the length and get a random character for each position for (i = 0; len > i; i += 1) { charIndex = Math.random() * charsetlen; id += charset.charAt(charIndex); } if (MES_UNIQUE_IDS[id] || (isNotInDOM && YAHOO.util.Dom.get(id))) { MES_UNIQUE_IDS[id] = true; // add DOM ids to the map return mes_generateUniqueId(charset, len, isNotInDOM); } MES_UNIQUE_IDS[id] = true; return id; };

First, setup a global object ‘MES_UNIQUE_IDS’ to be used to keep track of which IDs have already been generated by the function. The function ‘mes_generateUniqueId’ takes up to 3 optional parameters: a ‘charset’ string containing the characters used to generate your unique string (defaults to all upper- and lower-case latin characters and numbers), a length (’len’) for the IDs to be (default is 8), and ‘isNotInDOM’ governs whether to check if the generated ID exists in the DOM. The ‘for’ loop randomly chooses a character from the character set and appends it to the ID, up to the provided length. Then, if the ID already existed, or ‘isNotInDOM’ is true and the ID is in the DOM, we recursively call ‘mes_generateUniqueId’ until a unique ID is returned. Lastly, the ID is added to the unique ID object, so that it is never reused (until the page is refreshed).

One caveat of this function is when the ‘charset’ or ‘len’ are too small, then it is possible that all unique IDs are used up and ‘mes_generateUniqueId’ will cause too much recursion. Most browsers will automatically handle this and stop executing, but it would be best to use a ‘charset’ of at least 4 different characters, and a ‘len’ of 4 or more. However, the default setting will support up to 839299365868340224 unique combinations.

If you want to clear the unqiue IDs, simply delete ‘MES_UNIQUE_IDS’ and reset it to an empty object. Also, check out the Unique ID Generator Test Page for a practical example.

posted by Matt Snider at 12:32 am  

Wednesday, November 5, 2008

Questions and Requests

Some of the best articles this site has to offer are responses to user questions and/or widgets. I want to use this blog post, so you can submit widget requests and ask JavaScript questions. Whenever possible I will endeavor to address your issues in a blog post. Then I will append this article to include a link to the blog post answering your questions.

Please use the comment section to submit a request (please note that if this is your first time commenting, it may take up to 24 hrs for your comment to appear, before I get a chance to approve it).

posted by Matt Snider at 5:30 pm  

Saturday, November 1, 2008

Animation Widget - Slide Out From Top of Viewport

One of the most useful JavaScript animations is to animate a DOM node as it slides in from the top (or another side) of the browser viewport. I have implemented this animation several times for various projects, and thought that it would be a good idea to unify my implementations into one concise widget. I have used YUI as the Framework to power this widget, but unlike most of my other widgets, I have internalized all library references, so it would be easy to replace with another framework. In addition, I have not leveraged any of my own Framework files. By coding this way, the widget is completely standalone and more easily imported into your existing codebase. I intend to write all future widgets in this manner.

Anyway, this is my first version of ‘ViewportSlider’ and I am open to suggestions. Currently, it only supports animating out of the top of the viewport, but I have included a configuration object that I may later extend to support animations form any side of the viewport.

Example 1: Viewport Slider (source)

YAHOO.widget.ViewportSlider = function(elem, cfg) { // library namespace var YUI = YAHOO.util, Dom = YUI.Dom, E = YUI.Event, A = YUI.Anim; // local namespace var config = cfg || {}, F = function() {}, isAnimated = false, isSlideDown = false, that = null; // DOM namespace var dom = { node: Dom.get(elem) }; if (! dom.node) {return null;} // missing required // setup configuration // config.useFixed // config.center if (! config.easing) {config.easing = YAHOO.util.Easing.easeBoth;} if (! config.duration) {config.duration = 1;} if (! config.zIndex) {config.zIndex = 101;} // position for scroll Dom.setStyle(dom.node, 'visibility', 'hidden'); Dom.setStyle(dom.node, 'display', 'block'); Dom.setStyle(dom.node, 'position', 'absolute'); Dom.setStyle(dom.node, 'z-index', config.zIndex); var rect = Dom.getRegion(dom.node), height = rect.bottom - rect.top; Dom.setStyle(dom.node, 'top', '-' + height + 'px'); Dom.setStyle(dom.node, 'visibility', 'visible'); // event namespace var evt = { ieScroll: function() { var reg = Dom.getClientRegion(); Dom.setStyle(dom.node, 'top', reg.top + 'px'); } }; // public namespace F.prototype = { isAnimated: function() { return isAnimated; }, isSlideDown: function() { return isSlideDown; }, isSlideUp: function() { return ! isSlideDown; }, slideDown: function() { if (! isAnimated) { var rect = Dom.getRegion(dom.node), reg = Dom.getClientRegion(), vheight = Dom.getViewportHeight(), vwidth = Dom.getViewportWidth(), height = rect.bottom - rect.top, width = rect.right - rect.left, left = (vwidth - width) / 2; if (0 > left || isNaN(left)) {left = 0;} isAnimated = true; isSlideDown = true; Dom.setStyle(dom.node, 'top', reg.top - height + 'px'); Dom.setStyle(dom.node, 'position', 'absolute'); if (config.center) {Dom.setStyle(dom.node, 'left', left + 'px');} // setup the animation var anim = new A(dom.node, {top: {from: reg.top - height, to: reg.top}}, config.duration, config.easing); anim._onComplete.subscribe(function() { isAnimated = false; // fix the position, when configured and smaller than viewport if (config.useFixed && vheight > height) { // special case IE < 7, becuase it improperly supports "position:fixed" if (0 < YAHOO.env.ua.ie && 7 > YAHOO.env.ua.ie) { E.on(window, 'scroll', evt.ieScroll); } else { Dom.setStyle(dom.node, 'top', '0px'); Dom.setStyle(dom.node, 'position', 'fixed'); } } // don't position fixed, if viewport is too small }); anim.animate(); } }, slideToggle: function() { that[isSlideDown ? 'slideUp' : 'slideDown'](); }, slideUp: function() { if (! isAnimated) { var rect = Dom.getRegion(dom.node), height = rect.bottom - rect.top, reg = Dom.getClientRegion(); isAnimated = true; isSlideDown = false; Dom.setStyle(dom.node, 'top', reg.top + 'px'); // reposition to scroll offset + viewport before animating Dom.setStyle(dom.node, 'position', 'absolute'); // setup the animation var anim = new A(dom.node, {top: {from: reg.top, to: reg.top - height}}, config.duration, config.easing); anim._onComplete.subscribe(function() { isAnimated = false; Dom.setStyle(dom.node, 'top', '-' + height + 'px'); // remove special case event handler for IE < 7 if (0 < YAHOO.env.ua.ie && 7 > YAHOO.env.ua.ie) { E.removeListener(window, 'scroll', evt.ieScroll); } }); anim.animate(); } } }; that = new F(); return that; };

To instantiate, call as follows:

Example 2: Viewport Slider Instantiation

YAHOO.widget.ViewportSlider('idOfElementToAnimate', {center: true, useFixed: true, duration: 0.5});

The widget requires that a DOM node is provided, but the second parameter (configuration object) is entirely optional. Once call, ‘ViewportSlider’ first creates shortcut references to YUI, some internal variables, and a DOM reference to the node. Then it goes through the configuration parameters, setting default values as necessary. You can currently configure the following: center (true|false) - centers the node horizontally in the viewport, useFixed (true|false) - fixes node to the top of the viewport, duration (number) - the length of animation in seconds, easing (YAHOO.util.Easing.*) - any YAHOO easing method to use when animating, and zIndex (number) - the zIndex to make the node. Next we position the node absolutely above the top of the viewport and display it as block (in case it wasn’t a block-level node already). There are several public methods, the names of which explain there purpose, but check the source-code for additional documentation. To animate, call ’slideDown’, ’slideUp’, or ’slideToggle’.

When ’slideDown’ is called, we compute the dimension of the viewport, the node, and how far the page has been scrolled. Then we update the ‘isAnimated’ and ‘isSlideDown’ states, before positioning the element to the top of the viewport plus any scroll offset. This is so that a user who has scrolled down the page still see the animation. We also center the node, if the ‘center’ configuration property was set to true. Next we setup the animation to scroll from the top position we just set, to the top of the viewport plus any scroll offset. We subscribe to the YUI ‘_onComplete’ custom event so that when the animation terminates, we can adjust the ‘isAnimated’ state. We also check to see if ‘useFixed’ is set and that the size of the node is smaller than the viewport. When both are true, we position the element statically to the top of the viewport (’0px’), except in IE less than 7, where we have to use a special ’scroll’ event monitor to simulate static position (the node stays absolutely positioned and the top is adjusted to the scroll offset).

When ’slideUp’ is called, like ’slideDown’, we compute dimensions and adjust ‘isAnimated’ and ‘isSlideDown’ states. Then we reposition absolutely to the top of the viewport plus any scroll offset. This is required, because when ‘useStatic’ is set to true, the ‘top’ position of the node was adjusted to ‘0px’ and now must be restored before animating. We animate from the top of the viewport plus any scroll offset, to that value minus the height of the node. Using the same animation custom event subscription, as ’slideDown’, we adjust the ‘isAnimated’ state and reposition the element off where it was first positioned when the widget loaded. Lastly, if the IE less than 7 special-case listener was used, we unsubscribe it now, freeing up the resources.

When ’slideToggle’ is called, it uses the ‘isSlideDown’ to determine whether to call ’slideUp’ or ’slideDown’.

That is pretty much it, simple instantiate a ViewportSlider object, and use ’slideUp’ and ’slideDown’ to do all the work. Keep in mind that any node you pass to this widget will have their ‘position’ set to ‘absolute’ and be visually removed from the page, until ’slideDown’ is called. If the nodes are not initially ‘hidden’ or “display:none” then there will be a flicker (as seen on the demo page) as the widget loads. Here is a ViewportSlider Demo Page so you can try it out. The demo is straight forward, except there is a third button hidden way at the bottom of the page, used to illustrate how the ViewportSlider works with scrolling.

posted by Matt Snider at 5:46 pm  

Powered by WordPress