reptile7's JavaScript blog
Saturday, March 29, 2014
 
Holiday, It Would Be So Nice
Blog Entry #314

Welcome back to our discussion of the Java Goodies "Post Next Holiday" script. Having worked through the script's original, 1999-based code in the previous post, we will today update and generalize the script so that it's good for 2014 and beyond.

It is not possible to write a single Post Next Holiday script that applies to everyone. We will work with the 11 U.S. federal holidays as a common denominator* in this post as these holidays affect all Americans directly or indirectly. Moreover, the code below should be readily adaptable to holidays in other countries.

*I recognize that a lot of people don't get all of these days off and some people don't get any of them off, and you are of course free to subtract particular holidays if you want to do that.

With respect to determining their observation dates:
• Six U.S. federal holidays are pegged to a specific weekday: Birthday of Martin Luther King, Jr., Washington's Birthday, Memorial Day, Labor Day, Columbus Day, and Thanksgiving Day.
• Five U.S. federal holidays are pegged to a specific date and can therefore fall on a weekend: New Year's Day, Inauguration Day, Independence Day, Veterans Day, and Christmas Day.
We will separately address these holiday groups in the following sections.

Seize the day

The Birthday of Martin Luther King, Jr., holiday is observed on the third Monday of January; depending on what day of the week January begins on, this holiday could be January 15 or 16 or 17 or 18 or 19 or 20 or 21. We can easily nail down the actual date for any given year with:

var today = new Date( );
var thisyear = today.getFullYear( );

for (var i = 15; i < 22; i++) { var thirdweekday = new Date(thisyear, 0, i); if (thirdweekday.getDay( ) == 1) { var mlkday = thirdweekday; break; } }

I prefer the

new Date(year, month [, day, hour, minute, second, millisecond]);

constructor syntax to the

new Date(dateString);

constructor syntax, so that's what I'm gonna use. The getDay( ) method of the Date object returns 1 for a Monday. Mozilla's current break statement page is here.

Analogous snippets can be written for
Washington's Birthday, which is observed on the third Monday of February,
Memorial Day, which is observed on the last Monday of May,
Labor Day, which is observed on the first Monday of September,
Columbus Day, which is observed on the second Monday of October, and
Thanksgiving Day, which is observed on the fourth Thursday of November.

We previously used this approach to specify standard time ↔ daylight saving time transitions in the DST to the millisecond section of Blog Entry #103.

Save the date, part 2

The Christmas Day holiday is normally held on December 25; if December 25 is a Saturday or a Sunday, then this holiday is generally shifted to the preceding Friday or the following Monday, respectively. We can easily nail down the actual date for any given year with:

var christmas = new Date(thisyear, 11, 25);
if (christmas.getDay( ) == 6) christmas.setDate(24);
if (christmas.getDay( ) == 0) christmas.setDate(26);


The getDay( ) method returns 6 for a Saturday and 0 for a Sunday. The setDate( ) method of the Date object is documented here.

Analogous snippets can be written for
New Year's Day, which is normally held on January 1,
Independence Day, which is normally held on July 4, and
Veterans Day, which is normally held on November 11.
Given that New Year's Day follows Christmas by exactly one week, however, I prefer to get the New Year's Day holiday date with:

var nextnewyear = new Date(thisyear, 11, christmas.getDate( ) + 7);

The Inauguration Day holiday, which is normally held on January 20, is different from the other holidays in this group because
(a) most U.S. Government employees don't get it off,
(b) it only occurs once every four years,
(c) it's observed on Saturday if it falls on a Saturday (it goes to Monday if it falls on a Sunday), and
(d) it's not observed at all (on a different date) if it coincides with the Birthday of Martin Luther King, Jr., holiday.
That said, its holiday date can be determined with:

if (thisyear % 4 == 1) { // If it's an inauguration year var inauguration = new Date(thisyear, 0, 20); if (inauguration.getDay( ) == 0) inauguration.setDate(21); }

As shown above, inauguration years (2009, 2013, 2017, ...) can be conveniently flagged via a modulo operation.

Locating today amongst the holidays

If I had happened to be at work on July 5, 1999 (indday), then the original Post Next Holiday script would have told me that the Next holiday is Independence Day (July 5) vis-à-vis Labor Day, and I don't like that: I very much prefer to use a

(preceding or current holiday date <= today) && (today < next holiday date)

conditional for each next-holiday-setting if statement. Moreover, if we use the getDate( ) method to specify the date numbers in the holiday strings, then we won't need to update those strings every year.

if ((indday <= today) && (today < labor)) holiday = "Labor Day (September " + labor.getDate( ) + ")";

Upon generalizing the script, the if (today <= newyear) { holiday = "New Year's Day (Jan. 1)"; } statement for New Year's Day should be replaced with:

if ((christmas <= today) && (today < nextnewyear)) holiday = "New Year's Day (January " + nextnewyear.getDate( ) + ")";

However, this doesn't mean that we should throw out the newyear Date itself: we'll still need it for the newyear-to-mlkday period.

var newyear = new Date(thisyear, 0, 1);
if ((newyear <= today) && (today < mlkday)) holiday = "Martin Luther King's Birthday (January " + mlkday.getDate( ) + ")";


Our last task is to define a holiday setting for Inauguration Day; bearing in mind that the Inauguration Day holiday can coincide with or follow but not precede the Birthday of Martin Luther King, Jr., holiday, here's how we can do it:

if (inauguration && inauguration != mlkday) { var lastjanholiday = inauguration; if ((mlkday <= today) && (today < inauguration)) holiday = "Inauguration Day (January " + inauguration.getDate( ) + ")"; } else lastjanholiday = mlkday; if ((lastjanholiday <= today) && (today < washington)) holiday = "Washington's Birthday (February " + washington.getDate( ) + ")";

The last holiday in January (lastjanholiday) is usually the Birthday of Martin Luther King, Jr., holiday, but if it's an inauguration year AND if the Inauguration Day and Birthday of Martin Luther King, Jr., holidays do not coincide, then the lastjanholiday is Inauguration Day and a new holiday string is in order.

Demo

Place on your employee intranet to remind users of upcoming holidays automatically!


Sunday, March 23, 2014
 
Holiday Like It's 1999
Blog Entry #313

In today's post we will take up the "Post Next Holiday" script in the Scripts that Display Text section of the Java Goodies JavaScript Repository. Authored by Brian Pomeroy in late 1998, the Post Next Holiday script determines the holiday that most closely follows the current date and prints out a "Next holiday is …" message.

The Post Next Holiday script was formerly posted at www.javagoodies.com/postnextholiday.txt, but this page is now gone; fortunately, Joe Burns' online JavaScript Goodies book hosts the script here. The script's demo would work - you'd see the aforementioned message just below the "Place on your employee intranet …" paragraph - if it were 1999 or earlier, but it isn't.

The holiday landscape

Here in the U.S. there are 10 annual federal holidays. The Post Next Holiday script takes in seven of those holidays:
(1) New Year's Day
(2) Birthday of Martin Luther King, Jr.
(4) Memorial Day
(5) Independence Day
(6) Labor Day
(9) Thanksgiving
(10) Christmas
Not taken into account are:
(3) Washington's Birthday
(7) Columbus Day
(8) Veterans Day

The quadrennial Inauguration Day is also a federal holiday, or at least it is for Washington, D.C., and nearby parts of Maryland and Virginia.

Script content

In this section we will deconstruct the Post Next Holiday script per its original code; subsequently we will unfix the script from its 1999 mooring and bring the omitted holidays into the mix.

The script begins by creating Date objects for the current date and for the 1999 aforenoted holidays:

today = new Date( );
newyear = new Date("January 1, 1999");
mlkday = new Date("January 18, 1999");
memday = new Date("May 31, 1999");
indday = new Date("July 5, 1999");
labor = new Date("September 6, 1999");
thanksgiving = new Date("November 25, 1999");
christmas = new Date("December 24, 1999");


• Excepting May, the month parts of the dateString Date constructor arguments do not but should conform with the three-letter values of the month-name production in RFC 2822: January should be specified as Jan, July should be Jul, etc. For more on acceptable dateString arguments, see Mozilla's page on the parse( ) method of the Date object.

• The 1999 holiday dates can be verified by running cal 1999 on the command line - no need to go running to Wikipedia's 1999 pages.

• In 1999, July 4 was a Sunday and December 25 was a Saturday; the corresponding Independence Day and Christmas holidays were consequently observed on July 5 and December 24, respectively.

The Date constructor statements are followed by a set of if statements that determines where the today date falls relative to the newyear-christmas holidays:

if (today <= newyear) { holiday = "New Year's Day (Jan. 1)"; }
if ((today <= mlkday) && (today > newyear)) { holiday = "Martin Luther King's Birthday (Jan. 18)"; }
if ((today <= memday) && (today > mlkday)) { holiday = "Memorial Day (May 31)"; }
if ((today <= indday) && (today > memday)) { holiday = "Independence Day (July 5)"; }
if ((today <= labor) && (today > indday)) { holiday = "Labor Day (Sept. 6)"; }
if ((today <= thanksgiving) && (today > labor)) { holiday = "Thanksgiving Day (Nov. 25)"; }
if ((today <= christmas) && (today > thanksgiving)) { holiday = "Christmas Day (Dec. 24)"; }


If today precedes or is the newyear holiday, then a New Year's Day (Jan. 1) string is assigned to a holiday variable, meaning the next holiday is New Year's Day; if today precedes or is the mlkday holiday AND if today follows the newyear holiday, then the next holiday is Martin Luther King's Birthday (Jan. 18); and so on.

We can directly compare the various Date objects with comparison operators because a JavaScript Date object represents a single moment in time [relative to midnight] 1 January, 1970 UTC. The inner parentheses surrounding each comparison subcondition are unnecessary as comparison operators take precedence over the logical && (AND) operator, but there's no harm in holding onto them if you feel that they improve the code's readability.

A holiday message is written to the page by a separate if statement:

if (document.write) { document.write("<b>Next holiday is " + holiday + "</b>"); }

I've looked at my share of prehistoric JavaScript code and I have to say that this is the first time I've seen a document.write if gate. Such conditionalization is unnecessary as the write( ) method of the document object goes all the way back to JavaScript 1.0, the very first version of JavaScript: a browser that doesn't recognize document.write is a browser that won't know what to do with JavaScript in the first place.

The current versions of IE and Netscape in 1998 were IE 4.x and Netscape 4.x, respectively. Evidently the document.write( ) command was for the benefit of Netscape users; IE users could have written the holiday message to the innerHTML of a document.all("elementID") element. BTW, the </b> tag's </ character sequence is illegal, and should be escaped with a backslash (i.e., <\/b>).

Its 1999 dates notwithstanding, you can try out the original Post Next Holiday script by inputting a 1999 dateString into the today constructor: for example, a today = new Date("Mar 21, 1999"); starting point smoothly outputs a Next holiday is Memorial Day (May 31) holiday message.

Now, we could update the script by simply changing its various Date dateStrings and holiday strings - go here for an official list of 2014 federal holidays - but this is setting the bar rather low. Clearly, a better course of action would be to craft a more general script that tracks holidays not just for 2014 but for subsequent years as well, and we'll do just that in the next post.

Wednesday, March 05, 2014
 
Retooling the Tool Tip, Part 3
Blog Entry #312

In today's post we'll clean up the Java Goodies "Tip Box" script so that it works with Safari, Opera, and Firefox, as well as with Internet Explorer. We've duked it out with the script's Microsoft-implemented proprietary features over the last couple of entries; we'll hold onto some of these features but many of them will be sent packing in the discussion below.

Event listener registration

HTML 4 indirectly associates onmouseover and onmouseout with the document object in that it green-lights their use as attributes with the body element, which is a stand-in for the document object; HTML5 makes the association more explicit. Modern browsers are in sync with the current HTML onmouseover/onmouseout specification and therefore we can leave the document.onmouseover = toolTip; and document.onmouseout = unTip; registrations just as they are.

The event object

It would be a good idea to create a cross-browser event object for getting the mouseover/mouseout event target and for horizontally positioning the tool tip; this is conveniently accomplished via the ?: conditional operator, for example:

function toolTip(e) { var e = e ? e : window.event; ... }

Giving credit where credit is due, I learned this little trick from Apple's "Supporting Three Event Models at Once" tutorial.

Getting the event target

The srcElement property is supported by IE, Safari, and Opera, but not by Firefox. The Netscape-implemented target property, srcElement's standard equivalent, has cross-browser support although its IE support began only with IE 9. We can again use the ?: operator to make everybody happy:

var src = e ? e.target : e.srcElement;

Have you got a tip?

The src.tip if gate has an exact DOM equivalent:

if (src.hasAttribute("tip")) { ... }
// Better: if (src.getAttribute("tip")) { ... }


The hasAttribute(name) method of the Core DOM's Element interface returns true when an attribute with a given name is specified on [the calling] element or has a default value, false otherwise, quoting the W3C. Frustratingly, Dottoro reports that IE's hasAttribute( ) support does not predate IE 8. Fortunately, we can also use the Element interface's getAttribute(name) method, which was implemented by Microsoft for IE 4, to test for the presence of the tip attribute; getAttribute(name) returns the value of the calling element's name attribute, and as long as that value is not an empty string it will convert to true in a boolean context.

The src bottom

The var y = src.offsetTop + src.offsetHeight; determination can be left alone. CSS's top and height won't get you to the bottom of the Dignified's Domain link but offsetTop and offsetHeight will; moreover, the latter properties have cross-browser support and are themselves on track to be standardized.

Displaying the tool tip

Two entries ago I intimated that we shouldn't use the insertAdjacentHTML( ) method to render the tool tip because it's not supported by Mozilla's browsers; had I done a bit more homework, however, I would have learned that Firefox at least does support it (I haven't had Camino or Netscape 9 on my hard disk since I replaced my hard drive last fall so I can't vouch for those browsers). My bad.

But there's actually a more fundamental reason why we shouldn't use insertAdjacentHTML( ) for this operation. Like its appendChild( ) and insertBefore( ) DOM counterparts, insertAdjacentHTML( ) is meant to insert content into the normal flow of a document - Dottoro's insertAdjacentHTML( ) demo (which works very nicely with Firefox) does a good job of illustrating this - and we're not doing that: we're popping up a div that is removed from the normal flow by virtue of its absolute positioning. More appropriately we can
(a) code the div normally (in the document body HTML and not scriptically),
(b) initially set its CSS display to none, and then
(c) switch the display to block at runtime:

#zTip { display: none; position: absolute; ... }
...
document.getElementById("zTip").style.display = "block";
...
<div id="zTip"></div>


The src element's tip value can be loaded into the div via the innerHTML property (vide infra).

(Before moving on, here's another insertAdjacentHTML( ) problem:
With successive mouseovers
(1-2) Safari and Opera overwrite the zTip object but
(3-4) Firefox doesn't, and adds more <div id="zTip"></div>s to the end of the document body - those divs just sit there because Firefox 'moves' the zeroed-out first-created zTip object toward the src element - it is now clear to me that this also occurs with IE 5.1.7 and IE 4.5 on my iMac.)

Referencing the zTip div

Per the preceding code we should use the getElementById( ) method, and not the zTip id value, to reference the tool-tip div (getElementById( ) isn't supported by IE 4.x, but I would certainly like to think that no one out there is using IE 4.x in this day and age).

var tooltipDiv = document.getElementById("zTip");
tooltipDiv.innerHTML = src.getAttribute("tip"); ...


We can preface the statements that set the div's top/left offsets (and content, and width if desired) with tooltipDiv in lieu of the with construct; the statements'll be a bit longer, but that's a small price to pay to forestall any confusing bugs and compatibility issues, eh?

top/left offsets

The posTop/posLeft properties are supported by IE and Safari; contra Dottoro, Opera has dropped support for them, whereas Firefox never supported them in the first place. Naturally, we ought to exchange posTop/posLeft for their standard top/left counterparts.

For left-ing the zTip div, the window.event.x expression, which is supported by IE and Safari and Opera but not by Firefox, can be safely replaced by a cross-browser and standard e.clientX expression as long as the page width doesn't exceed the viewport width (and let's hope that's the case). If we have to do any horizontal scrolling to get to the src element, however, then we'll have to augment the e.clientX measurement with a document.body.scrollLeft term.

(The state-of-the-art way to get the mouseover x-coordinate relative to the left edge of the document content area is via the pageX property, which unfortunately is not supported by IE 5-8. For IE 5-8 we should be able to go back to the x property, but I am unable to confirm this; in practice, I find that e.x and e.clientX invariably give the same return with IE 5.1.7, Safari, and Opera.)

In sum, to bring everyone and everything on board - and remembering that top/left values have a string data type and require a unit identifier - the positioning code should be formulated as:

tooltipDiv.style.top = y + 14 + "px";
tooltipDiv.style.left = e.clientX + document.body.scrollLeft + 10 + "px";


The div width, revisited

An absolutely positioned div with a non-auto left value and auto width and right values should have a shrink-to-fit width (in most cases this will be the "preferred width", although it could be either the "available width" or the "preferred minimum width" if the div is close to the right edge of the viewport), and that's what I see for the zTip div with Safari, Opera, and Firefox, but it's not what I see with IE 4.5, which gives the div a width of 100% in the absence of a specific width setting, and I suspect that's why Ryan put the style.width = src.tip.length * 1.5; assignment in the with block. A shrink-to-fit width is in fact what we want, however, and if modern browsers are willing to set that width for us, then who are we to stand in their way?

Losing the tool tip

I'm sure you can rewrite the unTip( ) function at this point but I'll give you my code anyway:

function unTip(e) { var src = e ? e.target : e.srcElement; if (src.getAttribute("tip")) document.getElementById("zTip").style.display = "none"; }

With Safari and Opera title-induced tool tips disappear on their own (without mousing out from the tooltipped element) after about 10 seconds, whereas the corresponding Firefox tool tips hang around indefinitely. You can make the style.display = "block" zTip div disappear in 10 seconds by adding a window.setTimeout(function ( ) { unTip(e) }, 10000); command to the end of the if (src.getAttribute("tip")) { ... } block in the toolTip( ) function.

Let's see it...

We conclude with a demo incorporating the code presented in this entry (I've tweaked the zTip div's stylings a bit).

Move your mouse cursor over the link below to display a custom tool tip.

Blogger.com



Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)