1398 Part V ✦ Putting JavaScript to Work Dynamic HTML also offers some possibilities for this application. The entire pro- gram can be presented in a no-frame window, with the navigation, interactive con- tent, and instructions frames incorporated into individual positionable objects. The interactive content area can be treated almost like a slide show, with successive pages flying in from one edge. Not only is this application instructive for many JavaScript techniques, but it is also fun to play with as a user. Some financial Web sites have adapted it to assist visitors with investment decisions. You can use it to dream about where to go on a dream vacation, or help you decide the most ethical of a few paths confronting you in a personal dilemma. There’s something about putting in data, turning a crank, and watching results (with a bar chart to boot!) magically appear on the screen. ✦✦✦ Application: Cross-Browser DHTML Map Puzzle D ynamic HTML allows scripts to position, overlap, and hide or show elements under the control of style sheets and scripting. To demonstrate modern cross-browser DHTML development techniques, this chapter describes the details of a jigsaw puzzle game using pieces of a map of the “lower 48” United States (I think everyone would guess where Alaska and Hawaii go on a larger map of North America). I chose this application because it allows me to demonstrate several typi- cal tasks you might want to script in DHTML: hiding and showing elements; handling events for multiple elements; tracking the position of an element with the mouse cursor; absolute positioning of elements; changing the z-order of ele- ments; changing element colors; and animating movement of elements. As with virtually any programming task, the example code here is not laid out as the quintessential way to accomplish a particular task. Each author brings his or her own scripting style, experience, and implementation ideas to a design. Very often, you have available several ways to accomplish the same end. If you find other strategies or tactics for the opera- tions performed in these examples, it means you are gaining a good grasp of both JavaScript and Dynamic HTML. The Puzzle Design Figure 56-1 shows the finished map puzzle with the game in progress. To keep the code to a reasonable length, the exam- ple provides positionable state maps for only seven western states. Also, the overall design is intentionally Spartan so as to place more emphasis on the positionable elements and their scripting, rather than on fancy design. 56 56 CHAPTER ✦✦✦✦ In This Chapter Applying a DHTML API Scripting, dragging, and layering of multiple elements Event handling for three DOMs at once ✦✦✦✦ 1400 Part V ✦ Putting JavaScript to Work Figure 56-1: The map puzzle game DHTML example (Images courtesy Map Resources — www.mapresources.com) When the page initially loads, all the state maps are presented across the top of the puzzle area. The state labels all have a red background, and the silhouette of the continental United States has no features in it. To the right of the title is a ques- tion mark icon. A click of this icon causes a panel of instructions to glide to the cen- ter of the screen from the right edge of the browser window. That panel has a button that hides the panel. To play the game (no scoring or time keeping is in this simplified version), a user clicks and drags a state with the goal of moving it into its rightful position on the sil- houette. While the user drags the state, its label background to the right of the main map turns yellow to highlight the name of the state being worked on. To release the state in its trial position, the user releases the mouse button. If the state is within a four-pixel square region around its true location, the state snaps into its correct position and the corresponding label background color turns green. If the state is not dropped close enough to its destination, the label background reverts to red, meaning that the state still needs to be placed. After the last state map is dropped into its proper place, all the label back- grounds will be green, and a congratulatory message is displayed where the state map pieces originally lay. Should a user then pick up a state and drop it out of posi- tion, the congratulatory message disappears. I had hoped that all versions of the application would look the same on all plat- forms. They do, with one small exception. Because the labels are generated as posi- tioned DIV elements for all browsers, NN4 (especially on the Windows version) 1401 Chapter 56 ✦ Application: Cross-Browser DHTML Map Puzzle doesn’t do as good a rendering job as other browsers. If I were to use genuine LAYER elements for the labels just for NN4, they’d look better. And, while the code could use scripts to generate LAYERs for NN4 and DIVs for others, the choice here was to stay with DIV elements alone. If you try this game on NN4 and other DHTML browsers, you will see minor differences in the way the labels are colored (red, yel- low, and green) during game play. All other rendering and behavior is identical (although a rendering bug in NN6 is discussed later). Implementation Details Due to the number of different scripted properties being changed in this applica- tion, I decided to implement a lot of the cross-platform scripting as a custom API loaded from an external .js file library. The library, whose code is dissected and explained in Chapter 47, contains functions for most of the scriptable items you can access in DHTML. Having these functions available simplified what would have been more complex functions in the main part of the application. Although I frown on using global variables except where absolutely necessary, I needed to assign a few globals for this application. All of them store information about the state map currently picked up by the user and the associated label. This information needs to survive the invocations of many functions between the time the state is picked up until it is dropped and checked against the “database” of state data. That database is another global object — a global that I don’t mind using at all. Constructed as a multidimensional array, each “record” in the database stores sev- eral fields about the state, including its destination coordinates inside the outline map and a Boolean field to store whether the state has been correctly placed in position. Out of necessity for NN4, the state map images are encased in individual DIV ele- ments. This makes their positionable characteristics more stable, as well as making it possible to capture mouse events that NN4’s image objects do not recognize. If the application were being done only for IE4+ and W3C DOMs, the images themselves could be positionable, and the DHTML API could be used without modification. The custom API To begin the analysis of the code, you should be familiar with the API that is linked in from an external .js library file. Listing 47-2 contains that code and its description. The main program Code for the main program is shown in Listing 56-1. The listing is a long docu- ment, so I interlace commentary throughout the listing. Before diving into the code, however, allow me to present a preview of the structure of the document. With two exceptions (the map silhouette and the help panel), all positionable elements have their styles set via style sheets in the HEAD of the document. Notice the way class and id selectors are used to minimize the repetitive nature of the styles across so many similar items. After the style sheets come the scripts for the page. All of this 1402 Part V ✦ Putting JavaScript to Work material is inside the <HEAD> tag section. I leave the <BODY> section to contain the visible content of the page. This approach is an organization style that works well for me, but you can adopt any style you like, provided various elements that sup- port others on the page are loaded before the dependent items (for example, define a style before assigning its name to the corresponding content tag’s ID attributes). Listing 56-1: The Main Program (mapgame.htm) <HTML> <HEAD><TITLE>Map Game</TITLE> Most of the positionable elements have their CSS properties established in the <STYLE> tag at the top of the document. Positionable elements whose styles are defined here include a text label for each state, a map for each state, and a congrat- ulatory message. Notice that the names of the label and state map objects begin with a two-letter abbreviation of the state. This labeling comes in handy in the scripts when synchronizing the selected map and its label. The label objects are nested inside the background map object. Therefore, the coordinates for the labels are relative to the coordinate system of the background map, not the page. That’s why the first label has a top property of zero. While both the background map and help panel are also positionable elements, scripts need to read the positions of these elements without first setting the values. Recall that in the IE4+ and W3C DOMs, the style property of an object does not reveal property values that are set in remote style sheet rules. While IE5 offers a currentStyle property to obtain the effective property attributes, neither IE4 nor the W3C DOM afford that luxury. Therefore, the style sheet rules for the back- ground map and help panel are specified as STYLE attributes in those two elements’ tags later in the listing. <STYLE TYPE=”text/css”> .labels {position:absolute; background-color:red; layer-background-color:red; width:100; height:28; border:none; text-align:center} #azlabel {left:310; top:0} #calabel {left:310; top:29} #orlabel {left:310; top:58} #utlabel {left:310; top:87} #walabel {left:310; top:116} #nvlabel {left:310; top:145} #idlabel {left:310; top:174} #camap {position:absolute; left:20; top:100; width:1;} #ormap {position:absolute; left:60; top:100; width:1;} #wamap {position:absolute; left:100; top:100; width:1;} #idmap {position:absolute; left:140; top:100; width:1;} #nvmap {position:absolute; left:180; top:100; width:1;} #azmap {position:absolute; left:220; top:100; width:1;} #utmap {position:absolute; left:260; top:100; width:1;} 1403 Chapter 56 ✦ Application: Cross-Browser DHTML Map Puzzle #congrats {position:absolute; visibility:hidden; left:20; top:100; width:1; color:red} </STYLE> The next statement loads the external .js library file that contains the API described in Chapter 47. I tend to load external library files before listing any other JavaScript code in the page, just in case the main page code relies on global vari- ables or functions in its initializations. <SCRIPT LANGUAGE=”JavaScript” SRC=”DHTMLapi.js”></SCRIPT> Now comes the main script, which contains all the document-specific functions and global variables. Global variables here are ready to hold information about the selected state object (and associated details), as well as the offset between the position of a click inside a map object and the top-left corner of that map object. You will see that this offset is important to allow the map to track the cursor at the same offset position within the map. And because the tracking is done by repeated calls to a function (triggered by numerous mouse events), these offset values must have global scope. // global declarations var offsetX = 0 var offsetY = 0 var selectedObj var states = new Array() var statesIndexList = new Array() var selectedStateLabel As you will see later in the code, an onLoad event handler for the document invokes an initialization function, whose main job is to build the array of objects containing information about each state. The fields for each state object record are for the two-letter state abbreviation, the full name (not used in this application, but included for use in a future version), the x and y coordinates (within the coordi- nate system of the background map) for the exact position of the state, and a Boolean flag to be set to true whenever a user correctly places a state. I come back to the last two statements of the constructor function in a moment. Getting the data for the x and y coordinates required some legwork during devel- opment. As soon as I had the pieces of art for each state and the code for dragging them around the screen, I disengaged the part of the script that tested for accuracy. Instead, I added a statement to the code that revealed the x and y position of the dragged item in the statusbar (rather than being bothered by alerts). When I care- fully positioned a state in its destination, I copied the coordinates from the status- bar into the statement that created that state record. Sure, it was tedious, but after I had that info in the database, I could adjust the location of the background map and not have to worry about the destination coordinates, because they were based on the coordinate system inside the background map. // object constructor for each state; preserves destination // position; invokes assignEvents() function state(abbrev, fullName, x, y) { this.abbrev = abbrev this.fullName = fullName 1404 Part V ✦ Putting JavaScript to Work this.x = x this.y = y this.done = false assignEvents(this) statesIndexList[statesIndexList.length] = abbrev } // initialize array of state objects function initArray() { states[“ca”] = new state(“ca”, “California”, 7, 54) states[“or”] = new state(“or”, “Oregon”, 7, 24) states[“wa”] = new state(“wa”, “Washington”, 23, 8) states[“id”] = new state(“id”, “Idaho”, 48, 17) states[“az”] = new state(“az”, “Arizona”, 45, 105) states[“nv”] = new state(“nv”, “Nevada”, 27, 61) states[“ut”] = new state(“ut”, “Utah”, 55, 69) } The act of creating each state object causes all statements in the constructor function to execute. Moreover, they were executing within the context of the object being created. That opened up channels for two important processes in this appli- cation. One was to maintain a list of abbreviations as its own array. This becomes necessary later on when the script needs to loop through all objects in the states array to check their done properties. Because the array is set up like a hash table (with string index values), a for loop using numeric index values is out of the ques- tion. So, this extra statesIndexList array provides a numerically indexed array that can be used in a for loop; values of that array can then be used as index val- ues of the states array. Yes, it’s a bit of indirection, but other parts of the applica- tion benefit greatly by having the state information stored in a hash-table-like array. One more act of creating each state object is the invocation of the assignEvents() function. Because each call to the constructor function bears a part of the name of a positionable map object (composed of the state’s lowercase abbreviation and “map”), that value can be passed to the assignEvents() function, whose job is to assign event handlers to each of the map layers. While the actual assignment statements are the same for all supported browsers, assembling the references to the objects in each of the three DOM categories required object detection and associated syntax, very similar to the getObject() function of the API. In fact, if it weren’t for the NN4-specific mechanism for turning on event capture, this function could have used getObject() from the library. Here you can see the three primary user events that control state map dragging: Engage the map on mousedown; drag it on mousemove; release it on mouseup. These functions are described in a moment. // assign event handlers to each map layer function assignEvents(layer) { var obj if (document.layers) { obj = document.layers[layer.abbrev + “map”] obj.captureEvents(Event.MOUSEDOWN | Event.MOUSEMOVE | Event.MOUSEUP) } else if (document.all) { obj = document.all(layer.abbrev + “map”) } else if (document.getElementById) { 1405 Chapter 56 ✦ Application: Cross-Browser DHTML Map Puzzle obj = document.getElementById(layer.abbrev + “map”) } if (obj) { obj.onmousedown = engage obj.onmousemove = dragIt obj.onmouseup = release } } The engage() function invokes the following function, setSelectedMap(). It receives as its sole parameter an event object that is of the proper type for the browser currently running (that’s done in the engage() function, described next). This function has three jobs to do, two of which set global variables. The first global variable, selectedObj, maintains a reference to the layer being dragged by the user. At the same time, the selectedStateLabel variable holds onto a refer- ence to the layer that holds the label (recall that its color changes during dragging and release). All of this requires DOM-specific references that are generated through the aid of object detecting branches of the function. The last job of this function is to set the stacking order of the selected map to a value higher than the others so that while the user drags the map, it is in front of everything else on the page. To assist in establishing references to the map and label layers, naming conven- tions of the HTML objects (shown later in the code) play an important role. Despite the event handlers being assigned to the DIVs that hold the images, the mouse events are actually targeted at the image objects. The code must associate some piece of information about the event target with the DIV that holds it (“parent” types of references don’t work across all browsers, so we have to make the associa- tion the hard way). To prevent conflicts with so many objects on this page named with the lowercase abbreviations of the states, the image objects are assigned uppercase abbreviations of the state names. As setSelectedMap() begins to exe- cute, it uses object detection to extract a reference to the element object regarded as the target of the event ( target in NN4 and NN6, srcElement in IE). To make sure that the event being processed comes from an image, the next statement makes sure that the target has both name and src properties, in which case a lower- case version of the name is assigned to the abbrev local variable (if only IE4+ and W3C DOMs were in play here, a better verification is checking that the tagName property of the event target is IMG). That abbrev variable then becomes the basis for element names used in references to objects assigned to selectedObj and selectedStateLabel. Notice how the NN4 version requires a double-layer nesting to the reference for the label because labels are nested inside the bgmap layer. The presence of a value assigned to selectedObj becomes an important case for all three drag-related functions later. That’s why the setSelectedMap() func- tion nulls out the value if the event comes from some other source. /************************************************* BEGIN INTERACTION FUNCTIONS **************************************************/ 1406 Part V ✦ Putting JavaScript to Work // set global reference to map being engaged and dragged function setSelectedMap(evt) { var target = (evt.target) ? evt.target : evt.srcElement var abbrev = (target.name && target.src) ? target.name.toLowerCase() : “” if (abbrev) { if (document.layers) { selectedObj = document.layers[abbrev + “map”] selectedStateLabel = document.layers[“bgmap”].document. layers[abbrev + “label”] } else if (document.all) { selectedObj = document.all(abbrev + “map”) selectedStateLabel = document.all(abbrev + “label”) } else if (document.getElementById) { selectedObj = document.getElementById(abbrev + “map”) selectedStateLabel = document.getElementById(abbrev + “label”) } setZIndex(selectedObj, 100) return } selectedObj = null selectedStateLabel = null return } Next comes the engage() function definition. This function is invoked by mousedown events inside any of the state map layers. NN4 and NN6 pass an event object as the sole parameter to the function (picked up by the evt parameter vari- able). If that parameter contains a value, then it stands as the event object for the rest of the processing; but for IE, the window.event object is assigned to the evt variable. After setting the necessary object globals through setSelectedMap(), the next major task for engage() is to calculate and preserve in global variables the number of pixels within the state map layer at which the mousedown event occurred. By preserving these values, the dragIt() function makes sure that the motion of the state map layer keeps in sync with the mouse cursor at the very same point within the state map. If it weren’t for taking the offset into account, the layer would jump unexpectedly to bring the top-left corner of the layer underneath the cursor. That’s not how users expect to drag items on the screen. The calculations for the offsets require a variety of DOM-specific properties. For example, both NN4 and NN6 offer pageX and pageY properties of the event object, but the coordinates of the layer itself require left/top properties for NN4 and offsetLeft/offsetTop properties for NN6. A nested object detection takes place in each assignment statement. The IE branch has some additional branching within each of the assignment statements. These extra branches cover a disparity in the way IE/Windows and IE/Mac report the offset properties of an event. IE/Windows ignores window scrolling, while IE/Mac takes scrolling into account. Later calcula- tions for positioning must take window scrolling into account, so that scrolling is factored into the preserved offset global values if there are indications that the win- dow has scrolled and the values are being affected by the scroll (in which case the offset values go very negative). The logic is confusing, and it won’t make much sense until you see later how the positioning is invoked. Conceptually, all of these offset value calculations may seem like a can of worms, but they are essential, and are performed amazingly compactly. 1407 Chapter 56 ✦ Application: Cross-Browser DHTML Map Puzzle After the offsets are established, the state’s label layer’s background color is set to yellow. The function ends with return false to make sure that the mousedown event doesn’t propagate through the page (causing a contextual menu to appear on the Macintosh, for instance). // set relevant globals onmousedown; set selected map // object global; preserve offset of click within // the map coordinates; set label color to yellow function engage(evt) { evt = (evt) ? evt : event setSelectedMap(evt) if (selectedObj) { if (evt.pageX) { offsetX = evt.pageX - ((selectedObj.offsetLeft) ? selectedObj.offsetLeft : selectedObj.left) offsetY = evt.pageY - ((selectedObj.offsetTop) ? selectedObj.offsetTop : selectedObj.top) } else if (evt.offsetX || evt.offsetY) { offsetX = evt.offsetX - ((evt.offsetX < -2) ? 0 : document.body.scrollLeft) offsetY = evt.offsetY - ((evt.offsetY < -2) ? 0 : document.body.scrollTop) } setBGColor(selectedStateLabel,”yellow”) return false } } The dragIt() function, compact as it is, provides the main action in the applica- tion by keeping a selected state object under the cursor as the user moves the mouse. This function is called repeatedly by the mousemove events, although the actual event handling methodology varies with platform (precisely the same way as with engage(), as shown previously). Regardless of the event property detected, event coordinates (minus the previously preserved offsets) are passed the shiftTo() function in the API. Before the dragging action branch of the function ends, the event object’s cancelBubble property is set to true. In truth, only the IE4+ and W3C DOM event objects have such a property, but assigning a value to a nonexistent object prop- erty for NN4 does no harm. It’s important that this function operate as quickly as possible, because it must execute with each mousemove event. Canceling event bub- bling helps in a way, but more important, the cancellation allows the mousemove event to be used for other purposes, as described in a moment. // move DIV on mousemove function dragIt(evt) { evt = (evt) ? evt : event if (selectedObj) { if (evt.pageX) { shiftTo(selectedObj, (evt.pageX - offsetX), (evt.pageY - offsetY)) } else if (evt.clientX || evt.clientY) { shiftTo(selectedObj, (evt.clientX - offsetX), (evt.clientY - offsetY)) . 1398 Part V ✦ Putting JavaScript to Work Dynamic HTML also offers some possibilities for this application. The. dragging, and layering of multiple elements Event handling for three DOMs at once ✦✦✦✦ 1400 Part V ✦ Putting JavaScript to Work Figure 56-1: The map puzzle game DHTML example (Images courtesy Map. so many similar items. After the style sheets come the scripts for the page. All of this 1402 Part V ✦ Putting JavaScript to Work material is inside the <HEAD> tag section. I leave the <BODY>