reptile7's JavaScript blog
Thursday, July 22, 2010
 
You Can Count Me Out In
Blog Entry #185

For HTML Goodies' JavaScript Script Tip #64, we analyzed a script that uses (well, attempts to use) the document.cookie property to distinguish between first-time and return visitors to a Web page. In today's post, we will discuss HTML Goodies' "So You Want A Cookie Counter, Huh?" tutorial, which presents a script that extends the Script Tip #64 concept by using document.cookie to actually count the number of times a visitor has been to a Web page; the script thus serves as a simple Web counter for that visitor and page (not for all visitors to the page).

In brief, the "So You Want A Cookie Counter, Huh?" script writes to the user's hard disk a cookie whose value attribute value is a number n: n = 1 for a first-time visit, n = 2 for a second visit, etc.; just before the cookie is written, the script displays to the user a "You have been to my site n-1 time(s) before" message.

Joe provides a demo for the script in the tutorial's "The Effect" section. The demo currently does not work because the expires date of the cookie it sets - 4 April 2010 - is in the past; putting the expires date in the future sets things to right.

For a reason that is not clear to me, the tutorial's Cookie Count Script link to the script goes to a 404 page whereas its Cookie Count Script Explanation link to the script annotated with brief commentary works fine - both links should redirect to HTML Goodies' /legacy/beyond/javascript/ directory but only the latter does so in practice - something strange must be going on on the server side. The unannotated script can be accessed at http://www.htmlgoodies.com/legacy/beyond/javascript/cookiecountscript.html.

In the sections below, we'll first run through a quick script deconstruction and then I'll tell you how I would rewrite the script, with an up-to-date demo to follow.

First-time visit

The "So You Want A Cookie Counter, Huh?" script largely consists of two functions:
(1) a doCookie( ) function that writes a Counter_Cookie=n cookie; and
(2) a gettimes( ) function that returns an "n-1 time(s)" string.
Although it appears second in the script's main script element, the gettimes( ) function is called first, before the page has finished loading, by the following code in a separate script element in the document body:

document.write("<b>You have been to my site " + gettimes( ) + " before.</b>");

The gettimes( ) function first checks if there are any cookies associated with the current document; if not, it returns "0 times" to the gettimes( ) function call in the preceding document.write( ) command.
var cookie_name = "Counter_Cookie";
...
function gettimes( ) {
	if (document.cookie) { ... }
	return "0 times"; }
To be sure, if document.cookie is an empty string, then the user is definitely a first-time visitor. But there is a design problem here in that a non-empty document.cookie does not mean that the user is a return visitor; the user could have already picked up one or more cookies from a domain/path-related page: for example, if we were to surf to "So You Want A Cookie Counter, Huh?" for the very first time from the HTML Goodies home page, then document.cookie would contain cookies that were set at the latter page. As an if condition, document.cookie is thus an inadequate test for determining first-time vs. return visitor status. However, the if (document.cookie) { ... } block's first two lines spell out what we really want:

index = document.cookie.indexOf(cookie_name);
if (index != -1) { /* This is the if statement that should have an else clause returning "0 times", but it doesn't. */


The document.cookie.indexOf(cookie_name) command looks for the presence of Counter_Cookie, the name attribute value of the cookie we'll be writing in just a bit, in document.cookie; if it's there (if the index of the starting C is not -1), then we are probably dealing with a return visitor. This test isn't 100% foolproof either in that it's possible that another page could have added a Counter_Cookie cookie to document.cookie*, but it's as good as we're gonna get.

*Indeed, in the tutorial's "More Than One" section, Joe notes that the script could be added to every page of a Web site in order to count the total number of site pages visited by a user.

Anyway, let's assume that the user really is a first-time visitor whose document.cookie return vis-à-vis the current document is an empty string, and that "You have been to my site 0 times before" is printed out for the user as detailed above. Once the page has finished loading, the <body onload="doCookie( );"> element's onload event handler calls the doCookie( ) function in the main script element. Like the gettimes( ) function, the doCookie( ) function first checks if document.cookie does or does not contain any cookies and then determines the index of Counter_Cookie's starting C in document.cookie:
function doCookie( ) {
	if (document.cookie) {
		 index = document.cookie.indexOf(cookie_name); }
	else {
		 index = -1; }
As intimated earlier, an empty document.cookie converts to false as an if condition, and therefore -1 is assigned to index. But because an empty string can serve as the 'base string' for an indexOf( ) comparison, there's actually no need to conditionalize the index assignment: the if container and else clause can be thrown out, and the index = document.cookie.indexOf(cookie_name); statement can stand (and will dutifully return -1) on its own - this is also true for the corresponding code in the gettimes( ) function.

The next doCookie( ) line sets a date/time value for the expires attribute of the Counter_Cookie cookie:

var expires = "Monday, 04-Apr-2010 05:00:00 GMT";

The preceding expires value must of course be reset, but to what? Looking over my Safari cookie list, I see that five of those cookies (all of them from About.com, BTW) are set to expire in 2200 - sounds like a good year, eh? Moreover, Joe's expires format has too many details to keep track of for my tastes; I much prefer the toUTCString( )-based formulation given below:

var expires = new Date("Jan 1, 2200").toUTCString( );

We're finally ready to write the Counter_Cookie cookie:
if (index == -1) {
	document.cookie = cookie_name + "=1; expires=" + expires; }
The cookie's value attribute value is simply 1 - that's it.

Return visit

Now, what happens with a second visit, huh? The action shifts back to the gettimes( ) function and its if (index != -1) { ... } statement, whose condition now returns true.
if (index != -1) {
	countbegin = document.cookie.indexOf("=", index) + 1;
	countend = document.cookie.indexOf(";", index);
	if (countend == -1) {
		countend = document.cookie.length; }
	count = document.cookie.substring(countbegin, countend);
	if (count == 1) {
		return (count + " time"); }
	else {
		return (count + " times"); } }
À la other cookie scripts we've discussed - the Script Tip #64 script, the related Script Tips #60-63 script, and the "So, You Want To Set A Cookie, Huh?" script - the above code uses the indexOf( ) method to locate the Counter_Cookie cookie's value in the document.cookie string and the substring( ) method to extract that value, which is given a count identifier. ('Play-by-play' for the analogous code in the "So, You Want To Set A Cookie, Huh?" script's getName( ) function is given at the end of Blog Entry #144.) For a second visit, count is 1, "1 time" is returned to the gettimes( ) function call, and "You have been to my site 1 time before" is printed out for the user.

The doCookie( ) function is called when the page has loaded; index is no longer -1 and the else clause below is operative:
else {
	countbegin = document.cookie.indexOf("=", index) + 1;
	countend = document.cookie.indexOf(";", index);
	if (countend == -1) {
		countend = document.cookie.length; }
	count = eval(document.cookie.substring(countbegin, countend)) + 1;
	document.cookie = cookie_name + "=" + count + "; expires=" + expires; }
The above code writes a new Counter_Cookie cookie with a value again specifying the number of times the user has visited the page - 2 in this case. The current Counter_Cookie cookie value, 1, is located and extracted as in the gettimes( ) function; because that value has a string data type, it is numberified via the eval( ) function (the parseInt( ) function could also be used for this purpose) and then incremented to give the number 2, which is assigned to count. Lastly, cookie_name, =, count, ; expires=, and expires are concatenated to give
Counter_Cookie=2; expires=Wed, 01 Jan 2200 0x:00:00 GMT (x will vary depending on the user's time zone),
which replaces the corresponding Counter_Cookie=1 cookie when it is added to document.cookie.

I trust you can take it from here for subsequent visits.

Tightening up the code, and a demo

I can understand why Joe set up separate functions for writing the Counter_Cookie cookie and getting the number of user visits - 'division of labor' and all that - but you can see for yourself that this leads to some code redundancy; it would be better to utilize a single doCookie( ) function comprising an if (index == -1) { ... } block for first-time visitors followed by an else clause that takes care of return visitors, and I have accordingly merged the doCookie( ) and gettimes( ) functions for the demo below - check the page source for the details.




I myself am not a fan of Web counters. Counting page/site visits is something that should be done behind the scenes, IMO - you shouldn't bother the user with it. But 'different strokes for different folks', I suppose.

We'll revisit the topic of text scrolling in the following entry when we take up "So, You Want A JavaScript Ticker Tape, Huh?", the next Beyond HTML : JavaScript tutorial.

reptile7

Tuesday, July 13, 2010
 
Linking Modality
Blog Entry #184

In the previous post, we created a custom dialog box for Microsoft's "execCommand Example: Creating a Link" demo, and we are now ready to put that box in front of the user. We know that the window.open( ) method - at least in its classical form - will not allow us to coordinate the user's URL input with the demo's createLink command. However, we have another card to play.

The return, part 2

For MSIE 4, Microsoft introduced two methods that extend the window.open( ) concept:
(1) window.showModalDialog( ), which was implemented in MSIE for Windows and Mac; and
(2) window.showModelessDialog( ), which was implemented in MSIE for Windows only.

Like window.open( ), window.showModalDialog( ) and window.showModelessDialog( ) both open a new, secondary browser window. The showModalDialog( ) and showModelessDialog( ) windows are meant for the facile transfer of information between themselves and their opener windows - that's the "dialog" part. The showModalDialog( ) window retains the input focus while open. The user cannot switch windows until the dialog box is closed, i.e., when a showModalDialog( ) window is created, the user is locked into a single mode of operation - that's the "modal" part. In contrast, when a showModelessDialog( ) window is created, the user is free to toggle back and forth between the opener and secondary windows.

Microsoft's showModalDialog( ) page at best suggests, but does not clearly state, that window.showModalDialog( ) causes the operating system to block; in practice, window.showModalDialog( ) does indeed block the operating system with most of the browsers that support it. Intuitively, you wouldn't think that window.showModelessDialog( ) would block the system; as this method isn't supported by any of the browsers on my iMac, a session at my local library was necessary to verify that...ah, I'll get to it one of these days.

You can test your browser's support for window.showModalDialog( ) here and for window.showModelessDialog( ) here.

modal=yes

Playing catch-up, Mozilla subsequently implemented in Netscape 6 a modal window.open( ) feature that when set to yes|1 locks focus on a secondary window but does not equip it with any dialog capabilities beyond those available to a normal secondary window. According to Mozilla, the modal feature has since Mozilla 1.2.1 required the UniversalBrowserWrite privilege (ugh). Netscape 6.2.3 and Netscape 7.02 are installed on my defunct G3 iMac's hard disk and are based on pre-1.2.1 versions of the Mozilla Application Suite, so I went into the SheepShaver environment to see if these browsers would unprivilegedly act on the modal=yes feature and give me a modal new window. Yes! Success! But as expected, modal=yes is ignored by all of the OS X browsers on my computer.

showModalDialog( ) cross-browserization, sort of

Mozilla began support for the showModalDialog( ) method with Firefox 3. Safari also now supports the showModalDialog( ) method, although I don't know when that support started. Firefox, Safari, and MSIE 5.x for the Mac all implement showModalDialog( ) as it should be implemented: with these browsers, the opened new window is in fact modal, and the calling showModalDialog( ) command does cause the operating system to block. FYI, my preferred way to test for system blocking is to immediately follow the showModalDialog( ) command with a document.bgColor = "green"; assignment: if the opener document background turns green when the new window pops up, it ain't blocking.

Chrome supports the showModalDialog( ) method, but buggily: the opened new window is not modal (focus can be shifted to the opener window by clicking on it), but at least the operating system blocks.

Camino's 'support' for showModalDialog( ) is even shakier: a modeless new window pops up and the operating system does not block.

Opera does not support the showModalDialog( ) command and throws a Type mismatch error in response thereto.

Dialog method syntax

The showModalDialog( ) and showModelessDialog( ) methods share a common syntax, e.g.:

vReturnValue = object.showModalDialog(sURL [, vArguments] [, sFeatures])

As for window.open( ), the first dialog method parameter, sURL, specifies the URL of the document that is loaded into the new window.

The second dialog method parameter, vArguments, is for passing information from the opener window to the new window (unlike the second window.open( ) parameter, it doesn't specify a name for targeting purposes).

As for window.open( ), the third dialog method parameter, sFeatures, specifies a string of new window features. Microsoft currently defines eleven sFeatures features; many of these features reflect corresponding window.open( ) features: dialogWidth and dialogHeight respectively parallel window.open( )'s width and height, scroll maps onto window.open( )'s scrollbars, etc. In contrast to the window.open( ) features string, in which features are assigned values conventionally via the = operator and in which feature/value pairs are delimited with commas, the dialog method features string has a CSS-like syntax in which the feature and value of an individual feature/value pair are separated by a colon and in which feature/value pairs are delimited with semicolons:

vReturnValue = window.showModalDialog("myDocument.html", "", "dialogWidth: 500px; dialogHeight: 150px; dialogLeft: 200px; dialogTop: 100px;");
vs.
oNewWindow = window.open("myDocument.html", "", "width=500,height=150,left=200,top=100");

Practical notes

• In theory, the second and third dialog method parameters are independently optional; in practice, I find that if the second parameter is left out - if it isn't at least set to an empty string - then the third parameter will be ignored and the new window will be given a default size and position, which varies somewhat from browser to browser.

• At least on my computer, the dialog method features string is case-insensitive, e.g., dialogWidth can be typed as dialogwidth.

• The px unit identifiers in the above showModalDialog( ) features string are actually unnecessary but their inclusion is recommended by Microsoft for consistent results. BTW, Microsoft states that dialogWidth and dialogHeight (and presumably also dialogLeft/dialogTop) can be specified in any of CSS's length units - em, ex, px, in, cm, mm, pt, or pc - I haven't tried all of these but I can verify that in and cm do not work with Firefox, Safari, and Chrome (they do work with MSIE 5.x); it is best to stick to px in this regard.

• As shown above, the dialog method features string can contain whitespace à la a CSS style rule set (also check out the Method Syntax Used: fields on the aforelinked showModalDialog( )/showModelessDialog( ) demo pages). In contrast, the window.open( ) features string is not supposed to contain whitespace (at least according to Mozilla), and perhaps this is true on a PC, but I can confirm that a "width=500, height=150, left=200, top=100"-type string is OK on a Mac.

vReturnValue

The window.showModalDialog( ) method returns the value of the new window's returnValue property, another part of the 'dialog interface' implemented by Microsoft in MSIE 4. Here's how the returnValue property would fit into the "Creating a Link" demo:

(1) A new window holding the custom dialog box is opened via a showModalDialog( ) command.

// In the opener document:
var linkURL = window.showModalDialog("customDialog.html", "", "dialogWidth: 468px; dialogHeight: 128px;");
...
document.execCommand("createLink", false, linkURL);


(2) The user enters a URL into the box's id="input0" text field and clicks the OK button. The latter action calls a getURL( ) function that assigns the user's URL to window.returnValue and then closes the new window. The window.returnValue value is transferred to linkURL, the showModalDialog( ) return, which in turn is fed to the createLink command to create a new link.
<!-- In the openee document: -->
<input type="text" id="input0" name="myURL" value="http://" />
...
<button type="button" id="b1" onclick="getURL( );">OK</button>
...
function getURL( ) {
	window.returnValue = document.getElementById("input0").value;
	window.close( ); }
The returnValue property is thus analogous to window.prompt( )'s second parameter, which makes sense, because deploying a high-end prompt( ) box is very much what we're doing here. Of the showModalDialog( )-supporting browsers on my computer (vide supra), returnValue is supported by MSIE, Firefox, Safari, and Chrome (in the opener document, Camino gives a null returnValue return, as though it were an object reference).

Unlike window.showModalDialog( ) but like window.open( ), the window.showModelessDialog( ) method returns an object reference for the new window that it opens. However, the first Example on Microsoft's showModelessDialog( ) page outlines a procedure for sending a value from a showModelessDialog( ) window to its opener window - a procedure that is equally viable for showModalDialog( ) windows. The key to this procedure is the window.dialogArguments property, which is also part of Microsoft's implemented-in-MSIE-4 dialog interface.

If we pass a reference/value to a dialog window via the second dialog method parameter, then that reference/value will be assigned to the dialogArguments property of the new window. For example, if we had a document with a <title>Welcome, Visitor</title> title and that popped up a window.showModelessDialog("newDocument.html", document.title); window, then a window.alert(window.dialogArguments); command in the newDocument.html source would pop up an alert( ) box bearing a Welcome, Visitor message.

Interestingly, and as illustrated in the aforementioned showModelessDialog( ) example, if we were to pass a window object reference to a dialog window via the second dialog method parameter, then the dialogArguments property will allow us to access global variables and to call non-nested functions in the opener document via the use of 'qualified names'. Microsoft provides here a demo page for the example. The original demo doesn't work for me as a Mac user; however, upon changing the showModelessDialog( ) call to a showModalDialog( ) call, I got the example to work with Safari, Chrome, and MSIE 5.1.6; upon subsequently changing the innerText assignment to a textContent assignment, I got it to work with Firefox.

Here's how we could apply the example to the "Creating a Link" demo:

(1) Globally declare linkURL as an empty string prior to the AddLink( ) function:

var linkURL = "";

function AddLink( ) { ... }


(2) Pop up a modal window holding the customDialog.html box with the following command:

window.showModalDialog("customDialog.html", window, "dialogWidth: 468px; dialogHeight: 128px;");

(3) In the customDialog.html source, recast the getURL( ) function as:
function getURL( ) {
	var window0 = window.dialogArguments;
	window0.linkURL = document.getElementById("input0").value;
	window.close( ); }
In the above function, linkURL in effect functions as a property of the opener (window0) window. Indeed, for Firefox, Safari, and Chrome, I find that the window0 lines, and therefore the use of the dialogArguments property, can be replaced by an opener.linkURL = document.getElementById("input0").value; assignment (this works with Camino too but it throws an 'opener' is not an object runtime error with MSIE).

Demo

The div below holds a slightly modified version of the "Creating a Link" demo appearing at the end of Blog Entry #182. Upon selecting some second paragraph text and clicking the Click to add link button:
(a-d) MSIE, Firefox, Safari, and Chrome will pop up my custom dialog box, whose OK button uses the returnValue property to send the user's URL to the opener document;
(e-f) Opera and Camino will pop up the prompt( ) box of the Blog Entry #182 demo via the following conditional:
if (window.opera || navigator.userAgent.indexOf("Camino") != -1)
	var linkURL = window.prompt("Please enter your link URL into the field below.", "http://");
Select any portion of the following blue text, such as "My favorite Web site". Click the button to turn the selected text into a link.

My favorite Web site is worth clicking on. Don't forget to check out my favorite music group!


Here's the dialog box that appears in the window:

Hyperlink
Hyperlink Information


October 2016 update notes
• Firefox 48 will apply the foreColor execCommand( ) operation to non-link text but not to link text.
• Google Chrome 49 throws an Uncaught TypeError: window.showModalDialog is not a function when it hits the showModalDialog( ) command.
• Opera 23 does not support the window.opera object but does support the showModalDialog( ) method.
• On its window.showModalDialog( ) page Mozilla warns that the showModalDialog( ) method has been removed from the Web standards and is going away, and therefore my demo may not work for you if you are using a current browser. We should really be doing this with window.open( ) and I've thought up a way to get around window.open( )'s non-blocking problem but that's another post for another time.

And that will conclude our long and winding discourse on the execCommand( ) method. I was originally going to append to this post a section on the Microsoft Copy code that was briefly discussed at the beginning of Blog Entry #178, but it turns out that this code doesn't actually make use of the execCommand( ) method (it's based on a proprietary clipboardData object), so maybe we should just move on, huh? Accordingly, then, we will in the following entry check over the next Beyond HTML : JavaScript tutorial, "So You Want A Cookie Counter, Huh?" - we've done the cookie script thing a couple of times previously so we should be able to knock this one off in one post.

reptile7


Powered by Blogger

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