Matt Snider JavaScript Resource

Understanding JavaScript and Frameworks

Tuesday, February 26, 2008

CSS List Boxes Improved

I came across a slick CSS List Box widget the other day and wanted to share it. The original page was done by Mike Cherim, CSS: List Boxes. I liked how relatively simple the HTML and CSS was, plus the design was very visually appealing.

Example 1: Modified HTML

<div class=”clb-shell”> <h3>Web Services (List Boxes Example)</h3> <p>We offer a variety of web services for our customers. Our services include:</p> <ul> <li> <h5><a href=”#”>Web Hosting</a></h5> <p>We have professional web hosting services suited to your needs. Read more about our <a href=”#”>Web Hosting</a> services.</p> </li> <li> <h5><a href=”#”>Web Design</a></h5> <p>Need a web site? We can help you build your place on the web. Read more about our <a href=”#”>Web Design</a> services.</p> </li> <li> <h5><a href=”#”>Web Master</a></h5> <p>Need the works? Our Web master services take care of every need. Read more about our <a href=”#”>Web Master</a> services.</p> </li> </ul> <p>To learn more about these services, check out the links above or <a href=”#”>contact us</a> today!</p> </div>

I have changed very little of the HTML, as it was already well design and minimalist. One notable change is that the sectional heading tag are now H3 tags instead of H2 tags, and the H3 tags are now H5 tags. This follows the conventions I mentioned in last Fridays article, When to Use Heading Tags. Another change is that I removed the ID from the UL tag, because it is simply not necessary due to specificity (you may want to keep it in if you are using JavaScript to interact with the UL tag). The last change, is that I changed the “clb-shell” ID attribute into a CLASS attribute, this way you can use multiple instances of this widget on the same page.

You may be wondering why we use an anchor tag inside of the H5 tags. Ideally, this wouldn’t be necessary, but in IE 6 and lower you can only apply the pseudo class “:hover” to anchor tags. So if you like the hover style on the heading of the box, then this is necessary.

Assuming then, that you adopt my recommendations, you will need the following CSS:

Example 2: Modified CSS (assumes you are using Reset.css

/* First define a content are width and position, if needed. This centers (in most browsers) and applies a width */ div.clb-shell { margin : auto; width : 62.3em; } /* Default color, border of the heading */ div.clb-shell h3 { color : #669900; } /* P tags should pad and clear floats */ div.clb-shell p { padding : 1em; clear : both; } /* Always use EMs for the height, so that the boxes scale well. List-style-type: none should already be applied, but just in case Should be 4px thinner than the main container */ div.clb-shell ul { width : 61.9em; height : 9em; text-align : center; list-style-type : none; } /* Now I style the individual boxes (li) */ div.clb-shell ul li { margin : 0 0.2em; border : 1px solid #666; width : 20em; height : auto; background : #ffffea url(images/clb_li_back.jpg); /* Main background image */ float : left; display : inline; } /* Style the li links */ div.clb-shell a { color : #669900; } div.clb-shell a:hover, div.clb-shell a:focus, div.clb-shell a:active { color : #000; text-decoration : none; } div.clb-shell a:focus, div.clb-shell a:active { background-color : #fff; } /* Style the h5 links */ div.clb-shell ul h5 a { color : #ffffaa; display : block; width : 19.4em; padding : 0.2em 0.3em; background : #333 url(images/clb_h5_back.jpg) repeat-x; /* header background image */ border-bottom : 1px solid #666; text-decoration : none; } div.clb-shell ul h5 a:hover, div.clb-shell ul h5 a:focus, div.clb-shell ul h5 a:active { background : #957412 url(images/clb_h5_back_over.jpg) repeat-x; /* header hover image */ color : #fff; } /* This styles the text p content within the li separately. The most important thing here is to re-kill the padding and add the margin to create good gutters */ div.clb-shell ul p { font-size : 0.9em; padding : 0; margin : 1em; }

The only major change, beside the previously mentioned updates, is that I made to the styles was to rename the background images to clb_h5… instead of clb_h3. The other change is that since I assume you are using “reset.css”, we can safely use EMs instead of PXs. This allows your widgets to scale.

One other great benefit from using CLASS attributes instead of ID attributes is that you can easily override the styles here for different looks on the same page. For example, lets assume you use the ‘overload’ class to override the second CSS List Box:

Example 3: Override Colors

<ul class=”overload”>
div.clb-shell ul.overload li { background : #ffffea url(images/clb_li_back_2.jpg); /* Main background image */ } div.clb-shell ul.overload h5 a { background : #333 url(images/clb_h5_back_2.jpg) repeat-x; /* header background image */ } div.clb-shell ul.overload h5 a:hover, div.clb-shell ul h5 a:focus, div.clb-shell ul h5 a:active { background : #957412 url(images/clb_h5_back_over_2.jpg) repeat-x; /* header hover image */ }
posted by Matt Snider at 10:54 pm  

Saturday, February 23, 2008

When to Use Heading Tags

Heading tags are probably the most misused tag in all of HTML. Since the advent of CSS, many web-designers no longer care that an H1 tag is the most important tag of the page or that an H2 is the second most important tag. I have seen pages with multiple H1s and only a few H2s, or pages where HN (where N > 1) styles are bolder than H1 tags. You might be asking yourself, why should I care since CSS can masked these flaws? Well, here are few reasons:

  1. Semantic HTML - following this practice your HTML design should have meaning; so an H1 tag should mean the most important Heading on the page and H2 the second, etc
  2. Accessibility - Screen Readers do not understand the class or styles you have applied to your tags, they take meaning from context and assume the importance of a heading is based on the HTML specification (H1 > H2 > H3 …)
  3. No CSS - If a user turns their CSS off, your page won’t make as much sense as the browsers expect heading tags to follow HTML specifications
  4. Cleaner code - Generally, you need less HTML and CSS code when properly using tags

Points 1-3 can really be summed up by saying that your pages should make sense contextually, and with or without CSS. By writing semantic HTML your code is easier to be read by and shared with your colleagues. Typically, semantic HTML is cleaner as well. In this case, you can replace whatever you are using to show your page headings (I was using: “<div class=”header”>) with just <h1>.

When I redesigned some of my older work to be more semantic, I came up with these rules:

  • H1 - page heading/title, each page should have no more than 1 of these (except very rare situations)
  • H2 - page subheading/subtitle, should follow H1 tags; I reserve this tag for subheadings, because in most projects I use an optional subheading below my page headings.
  • H3 - section headings/titles, each content block on the page usually needs a heading and I reserve this tag for that purpose
  • H4 - section subheadings/titles, should follow H3 tags; again, I reserve this for a possible sectional subheading
  • H5 - mini-sections, inside of a section; often I style these only slightly large than the surrounding text and with an underline
  • H6 - minor-sections, often used when multiple ideas are expressed below an H5, but can be used in place of the H5 tag. Same style as H5, but no underline

When styling your tags, I use these general rules:

  • all your heading tags should be bolded and (slightly) larger than surrounding text
  • font-sizes should scale with H1 being the largest and H6 being the smallest
  • sometimes, I use the same font-size for the tags used in conjunction (ie, H1 size = H2, H3 size = H4, H5 size = H6), but that is up for designer interpretation
  • I like to use a color different from the rest of the text for H1 and H2 tags (and possibly a background color as well, depending on design), but use the standard text color for all other headings
  • I generally don’t use the underline text style for tags (except H5), instead if a heading needs to be underlined, then apply a bottom border line
  • and all heading tags should be block level

Following these guidelines, I find it easier to move between projects and make quick design changes. My markup is also simpler and I need less CSS selectors to change my heading styles than I previously needed when each DIV or heading tag had required a class or id selector to overwrite the default style. Lastly, my pages make a lot more sense when viewed by a Screen Reader or without CSS.

posted by Matt Snider at 12:17 pm  

Wednesday, February 20, 2008

YUI 2.5 Release

Lots of new and improvements on existing features. It also looks like they fixed the bug that was causing TextNodes to be treated as Arrays in the Dom.get() method.

YUI 2.5

posted by Matt Snider at 7:46 pm  

Tuesday, February 19, 2008

Numbers And Number Format Function

All numbers in JavaScript are 64bit (8 bytes) floating point numbers which yields an effective range of 5e-324 (negative) to 1.7976931348623157e+308 (positive). Because JavaScript is loosely typed and the plus operator also concatenates, you can easily convert JavaScript Numbers to Strings using: 1 + “”. JavaScript also has some built in number logic attached to the Number Object. However, most people never realize this, as they do not understand the difference between a JavaScript Number literal and a Number object.

Example 1: Literal vs. Object

var numericLiteral = 0; var numericObject = new Number(0); if (numericLiteral) { } // false because 0 is a falsy, will not be executed. if (numericObject) { } // true because numericObject exists as an object, will be executed.

The important distinction, as seen in the example above, is that Number objects are not falsy when they are ZERO. Since, they are Objects, they are truthy when they have any value except undefined and null. By default all Number variables (both numbericLiteral and numbericObject) have several functions available on their prototype:

Example 2: Number Functions

method IE FireFox Explaination toExponential 5.5 1.5 Returns the expotential value of the number. toFixed 5.5 1.5 Returns a number with a specified number of decimals. toLocaleString 3 2.0 Displays the number using regional preferences. toPrecision 5.5 1.5 Returns a string with a specified number of digits. toSource — 1.5 Returns the source code used to make the number. toString 3 2.0 Returns the number as a string. ValueOf 3 2.0 See toString

* Table From The Complete JavaScript Number Reference

The ‘toFixed’ and ‘toPrecision’ method come in very handy when converting Numbers to Strings with a given number of decimals or precision. However, they do not help when you want to use commas to represent thousands, millions, etc. For this task we will need to attach another method ‘format’ to Number, which will allow us to add not only comma, but also additional non-number characters (like ‘$’ and ‘%’).

Example 3: Format Function

/** * Formats the number according to the ‘format’ string; adherses to the american number standard where a comma is inserted after every 3 digits. * note: there should be only 1 contiguous number in the format, where a number consists of digits, period, and commas * any other characters can be wrapped around this number, including ‘$’, ‘%’, or text * examples (123456.789): * ‘0′ - (123456) show only digits, no precision * ‘0.00′ - (123456.78) show only digits, 2 precision * ‘0.0000′ - (123456.7890) show only digits, 4 precision * ‘0,000′ - (123,456) show comma and digits, no precision * ‘0,000.00′ - (123,456.78) show comma and digits, 2 precision * ‘0,0.00′ - (123,456.78) shortcut method, show comma and digits, 2 precision * * @method format * @param format {string} the way you would like to format this text * @return {string} the formatted number * @public */ Number.prototype.format = function(format) { if (! isType(format, ’string’)) {return ”;} // sanity check var hasComma = -1 < format.indexOf(’,'), psplit = format.stripNonNumeric().split(’.'), that = this; // compute precision if (1 < psplit.length) { // fix number precision that = that.toFixed(psplit[1].length); } // error: too many periods else if (2 < psplit.length) { throw(’NumberFormatException: invalid format, formats should have no more than 1 period: ‘ + format); } // remove precision else { that = that.toFixed(0); } // get the string now that precision is correct var fnum = that.toString(); // format has comma, then compute commas if (hasComma) { // remove precision for computation psplit = fnum.split(’.'); var cnum = psplit[0], parr = [], j = cnum.length, m = Math.floor(j / 3), n = cnum.length % 3 || 3; // n cannot be ZERO or causes infinite loop // break the number into chunks of 3 digits; first chunk may be less than 3 for (var i = 0; i < j; i += n) { if (i != 0) {n = 3;} parr[parr.length] = cnum.substr(i, n); m -= 1; } // put chunks back together, separated by comma fnum = parr.join(’,'); // add the precision back in if (psplit[1]) {fnum += ‘.’ + psplit[1];} } // replace the number portion of the format with fnum return format.replace(/[\d,?\.?]+/, fnum); };

See the comment block for help on how this function should behave. Basically, you can pass in a String as the format that contain any one number (in the format that you like) and the result will replace the number in the format String with the properly formatted Number object.

****** Update ******

All number variables have the Functions of “Number.prototype” available to them whether instantiated by call “new Number()” or simply setting a primitive to a variable. Only number primitives like “77″ do not have the prototype function.

posted by Matt Snider at 2:59 pm  

Friday, February 15, 2008

Too Much Recursion From Extending Objects

The other day I spent a couple hours beating my head, wondering why I was getting too much recursion in my objects. My project had 3 objects, where object C extended object B, and B extended object A. Each Object had a Function ‘update’, and the first statement inside ‘update’ was to call the parent object’s ‘update’ Function.

Example 1: Call to Function in Parent Object

.update(node) { this.parent.update.call(this, node); … },

I have been using “Core.extend” for a while and referencing the parent Object, but I had never tried to extend a child Object, while Function chain to the super class of the super class. In simpler terms, While I have often created an Object B extending an Object A, with a Function (such as ‘update’) that called the same Function in the parent Object A. I had never then also created an Object C extending Object B, which called a Function in B that then called a Function in A. For example:

Example 2: C Extends B Extends A

var A = function() {}; var B = function() {}; var C = function() {}; A.prototype = { update: function(obj) { // some logic } }; B.prototype = { update: function(obj) { this.parent.update.call(this, obj); // some logic } }; C.prototype = { update: function(obj) { this.parent.update.call(this, obj); // some logic } }; Core.extend(B, A); Core.extend(C, B);

Initially, this looks correct, as it is the same pattern that one always uses when extending and calling a Function on the parent Object. However, in this example, if you call the ‘update’ Function of C, you will get a “Too Much Recursion” error, or simply crash your browser. Why?

Well, when you call the parent Function, you generally pass in the scope of the current Object, that way any operations in the parent Function are applied to the current Object and not the prototype of the parent Object. However, if Object C passes its scope to Object B, then when the ‘update’ Function of Object B, calls its parent, it is actually calling the parent of Object C (since we are executing B in the scope of Object C). This then, creates an infinite loop.

I spent some time thinking and searching the web, but could not find an elegant way to change the extend Function to handle these situations. In the end, I had to hack the way Object C calls its parent Object B, in order prevent the loop. Here is what I did:

Example 3: Fixing the Infinite Loop

var A = function() {}; var B = function() {}; var C = function() {}; A.prototype = { update: function(obj) { // some logic } }; B.prototype = { update: function(obj, scope) { var that = scope || this; this.parent.update.call(that, obj); // some logic; use variable ‘that’ instead of ‘this’ to reference self } }; C.prototype = { update: function(obj) { this.parent.update.call(this.parent, obj, this); // some logic } }; Core.extend(B, A); Core.extend(C, B);

In these examples, each of the ‘update’ Functions expect a parameter ‘obj’. I have included this to illustrate that parameters can easily be passed into the Function chain. In my examples I have included only 1 parameter, but you could set the Functions up to accept no parameters or as many parameters as you like. However, to fix the scoping issue of Object B, the last parameter must be an optional ’scope’ variable.

With this technique, if you are using a non-extended instance of Object B, you can simply call the ‘update’ Function with the desired parameters and it works fine. Yet when you call the ‘update’ Function of Object B from its child (Object C), then the last parameter ’scope’ is passed and used as the operational scope inside the ‘update’ Function of Object B. Inside Object B, you will need to use the ‘that’ variable every time you mean ‘this’, except when calling the ‘update’ Function of its parent (Object A), as ‘that’ holds the true (operational) scope of the Function. Object C calls the ‘update’ Function of Object B with the execution scope of its parent (Object B). So the ‘update’ Function of Object B executes with the scope of Object B (allow access to the parent of Object B), while using the operational scope of Object C, stored in ‘that’. In this way, Object B can call its parent Object A, but we can manipulate the instance of Object C throughout the Function chain.

posted by Matt Snider at 11:23 am  

Wednesday, February 13, 2008

JavaScript Benchmarks

Brian Dillard over at Agile Ajax did some digging and found a great collection of JavaScript benchmarks. He did a great job collecting a variety of different tests and I found it very valuable:

JavaScript Benchmarks

About the only thing missing was Regex Benchmarks, which you can learn all about at:

Steven’s Blog

posted by Matt Snider at 12:20 pm  

Wednesday, February 13, 2008

Deferment Pattern

Sometimes a function you want to execute may have some internal variable that has yet to be loaded. The Deferment Pattern requires that you add a little extra code to the function, but allows you to call it at any time, forcing the function to wait until the internal variables are ready. For Mint.com I have developed an on-demand Dialog system that uses AJAX to retrieve the Dialog DOMs as needed. However, I often need to access the object immediately after instantiation, which is long before the server has returned the DOM. For situations like this, I find that, lately I have been leveraging the Deferment Pattern.

For example, suppose you have an alert Dialog that you want to retrieve from the server:

Example 1: Calling Dialog Manager

var dialog = Core.Widget.DialogManager.get(’alert’);

Assume that DialogManager is somewhere in your library and it initiates an AJAX request to retrieve the Dialog DOM, then returns a Dialog object. Somewhere in DialogManager this Dialog object will be cached and the DOM loaded appropriately when the AJAX request is returned. But in the the meantime you have the ‘dialog’ variable and you want to start manipulating it right away. Say, the Dialog object has a method ’show’, which makes the Dialog visible, and you want to call that function immediately:

Example 2: Showing the Dialog

var dialog = Core.Widget.DialogManager.get(’alert’); dialog.show();

Without the Deferment Pattern this would most likely crash (or do nothing), because the Dialog DOM is empty, as the AJAX request most likely has not returned yet. Let’s assume that when the AJAX request returns, the Object ‘dom’ is attached to the Dialog object and that this object contains a reference to the HTML element for the Dialog DOM associated to the ‘body’ key (dom = {body: element}). Therefore, the ’show’ method might be as simple as:

Example 3: Basic Show

dialog.show = function() { this.dom.body.style.display = ‘block’; };

Obviously, this function would fall into the pitfall mentioned above. Therefore, we add the Deferment Pattern, to force the function to wait until ‘dom.body’ is ready.

Example 4: Show Using Deferment Pattern

dialog.show = function() { var that = this; // deferment block; if the node doesn’t exist yet, wait for it if (! (this.dom && this.dom.body)) { setTimeout(function() {that.show.call(that);}, 250); return; } this.dom.body.style.display = ‘block’; };

In Example 4, the ’show’ Function tests to see if the ‘dom’ and ‘dom.body’ variables are set. When they are not, the function re-calls itself in a quarter of a second, using the native ’setTimeout’ method. Scope is maintained by using the ‘call’ Function of the ’show’ Function and passing in the scope of the original ’show’ Function. It will keep defering indefinitely, until the DOM objects are set, then it will execute the normal operation of the ’show’ Function.

This a great technique to use in conjunction with Lazy Function Definition Pattern, so that once the deferment is complete, you won’t need to execute that check again. This can be a performance boost if your ‘if’ statement operation is more complex than a simple existance test. You may also find it useful to send parameters into the function you are defering. Here is an example using the Lazy Function Definition Pattern and extra parameters:

Example 5: Show Improved

dialog.show = function(display) { var that = this; // deferment block; if the node doesn’t exist yet, wait for it if (! (this.dom && this.dom.body)) { // notice the parameter ‘display’ is passed here setTimeout(function() {that.show.call(that, display);}, 250); return; } // Lazy Definition Pattern this.show = function(display) { this.dom.body.style.display = display; }; this.show(display); };
posted by Matt Snider at 1:49 am  

Monday, February 11, 2008

News: Happy New Year 2008

Kung Hei Fat Choi (wishing you a prosperous new year)! Last week was Chinese New Year, which kept me busy since last Thursday (that and the new Mint rollout), so I was unable to write an article on Friday. However, I did keep up on the news over the weekend and here are some highlights:

Mint News

Last Thursday we rolled out a new version of Mint that supports investment accounts and uses the latest version of Really Simple History. We have also revamped the Add Account process, so check it out.

JavaScript News

posted by Matt Snider at 3:58 pm  

Tuesday, February 5, 2008

Using JSON Objects as RSH Location

In the past year, many sites have upgraded to use AJAX, loading dynamic content to their sites with JavaScript and some server side data format. This is a huge boon, to the UI as it is no longer the click and wait paradigm of Web 1.0. However, the downside of JavaScript loaded, dynamic content is that is not bookmarkable and breaks the history flow the client browser.

At the end of last year, we saw many companies competing to produce the best JavaScript history manager, including big names like Google and YAHOO. In the coming year, I believe most AJAX sites will start including a history mechanism. I am putting my support behind Really Simple History (RSH). As, I have mentioned before, RSH is simple, clean, and easy to use. It is also getting continued support by Brian Dillard of Agile Ajax.

Really Simple History

RSH is easy to use, and I believe Brian does a good job at explaining how it works. For the rest of this article, I will assume, you have everything included properly and understand basically how RSH works. You might have written code that looks something like the following for managing history on a page:

Example 1: RSH Module Example

var pageController = function() { // pageController Module Constant Variables var DEFAULT_LOC = ‘{”param1″:”value1″, “param2″: “value2″, “param3″: “value3″}’, LOC_PREFIX = ‘location:’; // default JSON object for location // pageController Module Local Variables var F = function() {}, that = null; // pageController Module Event Namespace var evt = { /** * Example event that adds a history value * * @method ajaxEventExample * @param e {event} the triggered event * @private */ ajaxEventExample: function(e) { // some logic on the event // assume logic creates value1, value2, and value3 var loc = PREFIX + ‘{”param1″:”‘ + value1 + ‘”, “param2″: “‘ + value2 + ‘”, “param3″: “‘ + value3 + ‘”}’; dhtmlHistory.add(loc, null); }, /** * A function that is called whenever the user presses the back or forward buttons. This function will be passed the newLocation, * as well as any history data we associated with the location. * * @method onHistoryChange * @param newLocation {string} the location * @param historyDate {object} the history * @private */ onChangeHistory(newLocation, historyData) { var loc = unescape(newLocation) || LOC_PREFIX + DEFAULT_LOC; if (loc && window.location.hash !== loc && -1 < window.location.indexOf(LOC_PREFIX)) { var json = unescape(loc.replace(LOC_PREFIX, ”)).parseJSON() || DEFAULT_LOC.parseJSON(); that.updateWidgetFunction(json.param1, json.param2, json.param3); } } }; // Public Functions and Variables F.prototype = { /** * An example update method * * @method updateWidgetFunction * @param param1 {string} value1 from location JSON * @param param2 {string} value2 from location JSON * @param param3 {string} value3 from location JSON * @public */ updateWidgetFunction: function(param1, param2, param3) { // update logic } }; // scope that variable to this that = new F(); /** START - RSH SETUP LOGIC **/ // add history listener function dhtmlHistory.addListener(evt.onHistoryChange); // fetch the current location, if any var initialLocation = dhtmlHistory.getCurrentLocation(); // if no location specified, use the default if (initialLocation) { evt.onHistoryChange(initialLocation, null); } /** STOP - RSH SETUP LOGIC **/ // return public methods return that; }();

This example shows a complete template for using RSH included in a page controller. Modify this to your liking and include it as the last file on your page (if not the end, then at least after rsh.js is included) to get RSH to work. Now let’s discuss how to use JSON with RSH:

First, prefix all locations with a unique string; in this example we use ‘location:’ as the prefix. The prefix is important for filtering “window.hash” changes so they do not break your code, as your history code path won’t execute if the location prefix is not present. It also, helps for readability of the location portion of the URL.

Second, when we use the history (add and update) we convert our parameters to a JSON object in String format. “json.js” will be included in the project anyway (or an equivalent package) for use with RSH, so you can leverage it to convert JSON objects to and from strings, otherwise, you can do it manually as I have in the ‘add’ function. It is important to unescape the retrieved location in the history callback function as the history code will have escaped it before adding it to “window.hash”.

Using these techniques you can now store your RSH location history as a JSON object, which is helpful if you typically use JSON objects to store JavaScript data or you were simply confused how this worked. If you prefer JSON objects, then you will have to do one less conversion in your code, and JSON objects are a robust and well-known format.

Hopefully, you also now have the understanding of RSH to modify this code to support storing the history as a query string or some other mechanism. Feel free to comment if you have any questions.

posted by Matt Snider at 8:44 pm  

Friday, February 1, 2008

YUI DateMath on JavaScript Date Object

In the Date Functions article, I mentioned that you could apply the methods on YAHOO.util.DateMath to the Date object direclty, instead of implementing a static class. A few people wrote in and requested that I do just that, so that is what I will write about today.

For reference, please check out the DateMath class at YAHOO:

DateMath

The first step is to remove the constants from the static class and augment the Date object with them.

Example 1: Date Augmentation

YAHOO.lang.augmentObject(Date, { /** * Constant field representing Day * @property DAY * @static * @final * @type String */ DAY : “D”, /** * Constant field representing Week * @property WEEK * @static * @final * @type String */ WEEK : “W”, /** * Constant field representing Year * @property YEAR * @static * @final * @type String */ YEAR : “Y”, /** * Constant field representing Month * @property MONTH * @static * @final * @type String */ MONTH : “M”, /** * Constant field representing one day, in milliseconds * @property ONE_DAY_MS * @static * @final * @type Number */ ONE_DAY_MS : 1000*60*60*24, /** * Retrieves a JavaScript Date object representing January 1 of any given year. * @method getJan1 * @param {Number} calendarYear The calendar year for which to retrieve January 1 * @return {Date} January 1 of the calendar year specified. */ getJan1 : function(calendarYear) { return Date.getDate(calendarYear,0,1); }, /** * Returns a new JavaScript Date object, representing the given year, month and date. Time fields (hr, min, sec, ms) on the new Date object * are set to 0. The method allows Date instances to be created with the a year less than 100. “new Date(year, month, date)” implementations * set the year to 19xx if a year (xx) which is less than 100 is provided. * * NOTE:Validation on argument values is not performed. It is the caller’s responsibility to ensure * arguments are valid as per the ECMAScript-262 Date object specification for the new Date(year, month[, date]) constructor. * * @method getDate * @param {Number} y Year. * @param {Number} m Month index from 0 (Jan) to 11 (Dec). * @param {Number} d (optional) Date from 1 to 31. If not provided, defaults to 1. * @return {Date} The JavaScript date object with year, month, date set as provided. */ getDate : function(y, m, d) { var dt = null; if (YAHOO.lang.isUndefined(d)) { d = 1; } if (y >= 100) { dt = new Date(y, m, d); } else { dt = new Date(); dt.setFullYear(y); dt.setMonth(m); dt.setDate(d); dt.setHours(0,0,0,0); } return dt; } });

Most of what we have moved here are constant values, but I did move two static methods. We moved “getJan1″, because the parameter required is not a Date object, but instead a year integer and therefore, doesn’t really belong on each instance of Date. The “getDate” method is a shortcut method to retrieve a Date object from 3 integers and therefore also does not belong on “Date.prototype”.

The following methods have been transformed to interact with the current Date object instantiation instead of requiring Date as a parameter.

Example 2: Date.prototype Augmentation

YAHOO.lang.augmentObject(Date.prototype, { /** * Adds the specified amount of time to the this instance. * @method add * @param {String} field The field constant to be used for performing addition. * @param {Number} amount The number of units (measured in the field constant) to add to the date. * @return {Date} The resulting Date object */ add : function(field, amount) { var d = new Date(this.getTime()); switch (field) { case Date.MONTH: var newMonth = this.getMonth() + amount; var years = 0; if (newMonth < 0) { while (newMonth < 0) { newMonth += 12; years -= 1; } } else if (newMonth > 11) { while (newMonth > 11) { newMonth -= 12; years += 1; } } d.setMonth(newMonth); d.setFullYear(this.getFullYear() + years); break; case Date.DAY: d.setDate(this.getDate() + amount); break; case Date.YEAR: d.setFullYear(this.getFullYear() + amount); break; case Date.WEEK: d.setDate(this.getDate() + (amount * 7)); break; } return d; }, /** * Subtracts the specified amount of time from the this instance. * @method subtract * @param {Number} field The this field constant to be used for performing subtraction. * @param {Number} amount The number of units (measured in the field constant) to subtract from the date. * @return {Date} The resulting Date object */ subtract : function(field, amount) { return this.add(field, (amount*-1)); }, /** * Determines whether a given date is before another date on the calendar. * @method before * @param {Date} compareTo The Date object to use for the comparison * @return {Boolean} true if the date occurs before the compared date; false if not. */ before : function(compareTo) { return (that.getTime() < compareTo.getTime()); }, /** * Determines whether a given date is after another date on the calendar. * @method after * @param {Date} compareTo The Date object to use for the comparison * @return {Boolean} true if the date occurs after the compared date; false if not. */ after : function(compareTo) { return (that.getTime() > compareTo.getTime()); }, /** * Determines whether a given date is between two other dates on the calendar. * @method between * @param {Date} dateBegin The start of the range * @param {Date} dateEnd The end of the range * @return {Boolean} true if the date occurs between the compared dates; false if not. */ between : function(dateBegin, dateEnd) { return (this.after(dateBegin) && this.before(dateEnd)); }, /** * Calculates the number of days the specified date is from January 1 of the specified calendar year. * Passing January 1 to this function would return an offset value of zero. * @method getDayOffset * @return {Number} The number of days since January 1 of the given year */ getDayOffset : function() { var beginYear = Date.getJan1(this.getFullYear()); // Find the start of the year. This will be in week 1. // Find the number of days the passed in date is away from the calendar year start return Math.ceil((this.getTime() - beginYear.getTime()) / Date.ONE_DAY_MS); }, /** * Calculates the week number for the given date. This function assumes that week 1 is the * week in which January 1 appears, regardless of whether the week consists of a full 7 days. * The calendar year can be specified to help find what a the week number would be for a given * date if the date overlaps years. For instance, a week may be considered week 1 of 2005, or * week 53 of 2004. Specifying the optional calendarYear allows one to make this distinction * easily. * @method getWeekNumber * @return {Number} The week number of the given date. */ getWeekNumber : function() { var date = this.clearTime(); var nearestThurs = new Date(date.getTime() + (4 * Date.ONE_DAY_MS) - ((date.getDay()) * Date.ONE_DAY_MS)); var jan1 = Date.getJan1(nearestThurs.getFullYear()); var dayOfYear = ((nearestThurs.getTime() - jan1.getTime()) / Date.ONE_DAY_MS) - 1; return Math.ceil((dayOfYear)/ 7); }, /** * Determines if a given week overlaps two different years. * @method isYearOverlapWeek * @return {Boolean} true if the date overlaps two different years. */ isYearOverlapWeek : function() { var nextWeek = this.add(Date.DAY, 6); return (nextWeek.getFullYear() != weekBeginDate.getFullYear()); }, /** * Determines if a given week overlaps two different months. * @method isMonthOverlapWeek * @return {Boolean} true if the date overlaps two different months. */ isMonthOverlapWeek : function() { var nextWeek = this.add(Date.DAY, 6); return (nextWeek.getMonth() != weekBeginDate.getMonth()); }, /** * Gets the first day of a month containing a given date. * @method findMonthStart * @return {Date} The JavaScript Date representing the first day of the month */ findMonthStart : function(date) { return Date.getDate(this.getFullYear(), this.getMonth(), 1); }, /** * Gets the last day of a month containing a given date. * @method findMonthEnd * @return {Date} The JavaScript Date representing the last day of the month */ findMonthEnd : function() { var start = this.findMonthStart(); var nextMonth = start.add(Date.MONTH, 1); return nextMonth.subtract(Date.DAY, 1); }, /** * Clears the time fields from a given date, effectively setting the time to 12 noon. * @method clearTime * @return {Date} The JavaScript Date cleared of all time fields */ clearTime : function() { var date = new Date(this.getTime()); date.setHours(12,0,0,0); return date; } });

Generally these functions do not modify the current date, but instead return a new instance of the Date object after a transformation has been made. So if you wanted to update the current Date instantiation, then you would need to assign it to the returned value:

Example 3: Assignment

var d = new Date(); // d is now the current time Date object d = d.findMonthStart(); // d is now the first of the month Date object // versus var date = new Date(); // date is now the current time Date object var newdate = date.findMonthStart(); // newdate is now the first of the month Date object, but date is still the current time

The other functions return boolean values and should be straight forward in meaning. I also took the liberty to simplify the code. YUI tends to be more verbose than necessary, most likely as a result of Douglas Crockfords JavaScript standards (no function chaining and always assigning a value to a variable).

I have not created a test page for this code, but it does compile and my quick function test seemed to work as expected. Please let me know if you notice anything that does not work as expected.

posted by Matt Snider at 6:26 pm  

Powered by WordPress