Saturday, November 18, 2006

Fun With The DOM...Playing With The Action Bar

I promised some more technical articles awhile back and haven't delivered on that promise yet, so...here you go. I'm going to stray from my Notes client focus this time to talk about web development. In this post, I'm going to spend a little time dissecting the Domino-generated action bar as it is represented in the Document Object Model (DOM). While many blog authors have focused on ways to modify the look and feel of the action bar, if you utilize the power of the DOM, you can do much more. Like what, you may ask? How about:

Generating a right-click menu...



...or generating a dynamic drop-down menu...



...or whatever you can think up. Basically, if you know how to utilize the DOM to get a reference to the action bar object, you are limited only by your imagination. I'm sure smarter people than I can come up with even better techniques. But, since this is my blog, I'll give it the old college try and explain things as best I can.

My idea when putting this technique together was to allow the Domino developer coming from a Notes client background to write action buttons as they always do...no need to write special code for the web. Of course, it's my opinion that you should learn as much as you can about web techniques. When you do, you'll find yourself relying less and less on Domino-generated code. However, there's something quite satisfying about whipping out a line or two of formula language and having the code execute in the browser. If you think so too, read on.

When Domino generates the action bar (on a form, view, page, whatever), the underlying HTML that is created is a table. Each action button that exists on the design element is housed in its own table cell. To see this in action, just view the source (in your browser) of any page that has an action bar. Let's take an example view with three action buttons: "Expand", "Collapse" and "Create New Document". For now, the code behind these buttons is unimportant. What we care about is the HTML output. In this example, it would look something like this:


<table border="1" cellspacing="2" cellpadding="2">
<tr valign="middle">
<td><a onclick="showHide('descRow', 1) return false;" href="">Expand</a></td>
<td><a onclick="showHide('descRow', 0) return false;" href="">Collapse</a></td>
<td><a href="/DHTML2006.nsf/New?OpenForm" target="_self">Create New Document</a></td>
</tr>
</table>


As you can see, Domino creates a table with a single row and then generates three <td>s...one for each action. So far, so good.


Representations in the DOM

One thing that I've found when working with other Domino developers is although they have probably used techniques that manipulate the DOM in their applications, many don't really know about the DOM or understand what its about. Here's my (probably oversimplified) version: The Document Object Model describes an HTML document in a tree form. It basically allows you to use an object-oriented methodology to parse HTML and to operate on the contents of that HTML. When a page loads in your browser, the browser creates a hierarchical representation of all of the HTML elements. This hierarchical representation is often called the Document Tree. The document tree consists of "nodes", which are basically objects that have properties and methods. There are several types of nodes in the DOM, but usually we work with either element nodes or text nodes. An element node is a node on the Document Tree that was created by an individual HTML tag. For example, a <div> element in your code will create an element node branch of the tree. A text node is a node that is created by some general content...usually text. It's interesting to note that nodes can have many relationships since the DOM is hierarchical in nature. An element node can have child nodes, for example, or siblings, a parent, etc.

Let's take a look at a simple example that might explain this a bit better.

Assume we have a standard unordered list describing what happens at a typical rock concert from the perspective of the roadies.


  • Sound Check
  • Main Event
  • Break Down


In HTML, this is written as

<ul>
<li>Sound Check
<li>Main Event
<li>Break Down
</ul>


Represented as a document tree, it would look something like this:



The opening <ul> tag creates a new branch in the document tree...a new element node. This element node has three children, the <li> elements. These in turn have child elements as well...the text nodes that represent the content within the <li> tags ("Sound Check", "Main Event" and "Breakdown"). Pretty cool, yes?

Finding objects within the document tree is a matter of traversing the various nodes, using the relationship between elements. For example, if I have an object representing the first <li> element (let's say it has an ID of "node0"), then if I want to retrieve its parent, I would write node0.parentNode, which would give me an object representing the <ul> tag. If I wanted to get the text inside the middle <li> tag in our concert example, I could traverse the tree like so:


//Line 1 (assuming in our example that we want the first <ul> on the page)

var theList = document.getElementsByTagName('ul')[0];

//Line 2 (this would result in an alert box with the text "Main Event")

alert(theList.childNodes[1].firstChild.nodeValue);





Let's break this down:

Line 1 uses getElementsByTagName to return an array of objects representing all the <ul> tags on the page. We want the first <ul>, so we use subscript 0 to get the first object in the array.

Line 2 uses some of the node relationships to get what we want. childNodes returns an array of all the children of a given element node, while firstChild returns just that...the first child node. Thus, we are getting the second child of theList (the second <li> tag) and then the first child of that element (the text node that holds the content we want). Finally, nodeValue returns the actual text inside the node (i.e. "Main Event").

I don't want to go too deep here, so suffice it to say that there are many of these node relationships available (firstChild, lastChild, nextSibling, etc.) and it is quite worthwhile to learn them.

You can see from this fairly simple example that the document tree can get complex very quickly. A far easier way to traverse the tree is to use an element's ID attribute. You've probably done this before. The JavaScript to get a particular object is simply document.getElementById('idvalue'), where 'idvalue' is the ID attribute of the object in question. It is highly recommended that you give your elements ID attributes...it will make your life much easier. Going back to the simple example, let's put some IDs in and re-write the code.


<ul ID="concertList">
<li ID="firstPhase">Sound Check
<li ID="secondPhase">Main Event
<li ID="thirdPhase">Break Down
</ul>


Now to get the text in the middle <li>, we could simplify our code to:


alert(document.getElementById('secondPhase').firstChild.nodeValue);


Ah...much cleaner!

In addition to element nodes and text nodes, there is a special kind of node known as an attribute node. An attribute node is not really a part of the document tree, but it does provide a handy way of getting at attributes for a given element node. If you wanted to change the 'href' attribute for a link on a page from www.google.com to www.yahoo.com, you could do this by manipulating the attribute node of the <a> object. Assume the link had an ID of "thelink". The JavaScript to do this would then be: document.getElementById('thelink').setAttribute('href', 'http://www.yahoo.com'). You can manipulate attributes using:

setAttribute()
createAttribute()
getAttribute()
removeAttribute()
etc.

Again, I'd recommend looking into these functions in more detail...


Aside: To learn more about the DOM, check out the following great book:

Stuart Landridge, DHTML Utopia: Modern Web Design Using JavaScript & DOM (SitePoint, 2005).


OK...at this point you're thinking..."Great...thanks for all the boring DOM mumbo jumbo...on with the good stuff, geek boy!" I promise that this is important, though, since it will make manipulating the action bar seem really easy. So let's get to it, shall we?

As I mentioned at the top of the post, the Domino translation engine outputs the action bar as a table and it is always the first table on the page. So the first thing we want to do when manipulating the action bar is to get this object.


var actionBarTable = document.getElementsByTagName('Table')[0];


Next, we want to get all the <a> elements, since these hold the actual code to execute and the labels for the action buttons. Again, we'll use getElementsByTagName to return an array of all the <a> elements that are within the table.


var actionAnchors = actionBarTable.getElementsByTagName('a');


Now the fun begins...and this is where your imagination can come up with some great uses for the objects we have in the actionAnchors array. Usually, you'll iterate through the objects in this array to pull out the onClick events or href attributes and also grab the labels. So...how could you get information from the first action button?

To get the label, you would traverse the DOM:


actionAnchors[0].firstChild.nodeValue //remember...the <a> tag creates an element node and it has a child node which is the label text


To determine what the action actually does, you can check if it has an onClick event (if you coded your action button with JavaScript, then this will be true). If it doesn't, then it will have an href attribute (usually used to trigger an @Formula).


if (actionAnchors[0].onClick) {
do something with the onClick attribute;
}
else {
do something with actionAnchors[0].href;
}



Now, it's time to put these ideas together into a concrete example. Here's a sample database you can use if you want to follow along.

We are going to use the action bar generated by Domino to create a simple CSS drop-down menu. There are many freely available menus out on the internet and I encourage you to use the great work others have done. Don't reinvent the wheel...just use Domino to leverage it's power. CSS Menus are generally regarded as the most flexible and "safe" kind of dynamic menus since they rely on standard HTML tags which allow the menu to degrade gracefully in the event there is a problem with the CSS or the browser does not support a feature that is used. There are many excellent CSS menus available on the internet and a quick search on your favorite search engine will turn these up. For purposes of this demo, I'm using CSS Express Menus from projectseven.com.

To get started, there are a couple of downloads to grab from the projectseven website (these are already in the example database). You'll find the CSS (which is quite simple and elegant) in the page titled 'p7exp.css'. There is also a small snippet of javascript that is used to address some shortcomings in IE (p7exp.js). The structure of the dynamic menu is simply built with an unordered list. You can see a hardcoded example of this structure by examining the "StaticCSSMenu" form in this database. Here's the basic idea:


<div id="menuwrapper">
<ul id="p7menubar">
<li><a href="#">Home</a></li>
<li><a class="trigger" href="#">Trigger One</a>
<ul>
<li><a href="#">Sub 1.1</a></li>
<li><a href="#">Sub 1.2</a></li>
<li><a href="#">Sub 1.3</a></li>
<li><a href="#">Sub 1.4</a></li>
</ul>
more code here...
</li></ul>
<br class="clearit">
</div>


Note that the menu is a single, nested unordered list. Each link that acts as a trigger to show a sub-menu is assigned a class="trigger". The trigger class sets a downward-pointing arrow as a background image, indicating that a sub-menu is available. The code looks like this:


<li><a class="trigger" href="#">Trigger One</a>


Viewed without CSS, this code gives you the following:



It's fairly easy to style this unordered list, then, to provide the functionality we're looking for. So, the plain unordered list magically becomes:



Hopefully, at this point you are thinking you are thinking to yourself, "Self...with Notes, I can do all sorts of cool list processing and text manipulation. I bet I could dynamically create the unordered list and have a customized, on-the-fly CSS menu." Well, you are one smart cookie, since that is exactly what we are going to do...using the action bar as our source.

Let's review the first step in the process. This part is generic to any solution you would use to manipulate the action bar. The action bar in design mode for our example looks like this:



1. Iterate through the <a> tags in the Action Bar and create three arrays; one to hold the a flag that tells us what the action is (an href or onClick), one to hold the actual code and one to hold the label.


var whatAction = new Array();
var contextAction = new Array();
var labelAction = new Array();

var actionBarTable = document.getElementsByTagName("Table")[0];
var actionAnchors = actionBarTable.getElementsByTagName("a");
for (i=0; i < actionAnchors.length; i++) {
if(actionAnchors[i].onclick) {
contextAction[i] = actionAnchors[i].getAttribute("onclick").toString
().replace(/^function anonymous\(\)\n{\n/,"").replace(/\n}$/,"");
contextAction[i] = contextAction[i].replace(/return false/, "");
whatAction[i] = "click";
}
else {
contextAction[i] = actionAnchors[i].href;
whatAction[i] = "href";
}
labelAction[i] = actionAnchors[i].firstChild.nodeValue
}


As you can see, this code loops through the actionAnchors object, which is an array of all the <a> elements in the action bar table. In this loop, we check if the <a> element that is being processed has an onclick event. If it does, we use the DOM to get the onclick attribute and place it into the contextAction array. I'm doing some string manipulation at the same time in order to remove the function definition that the browser automatically sets up for the onClick event (see aside for more detail). If the action doesnt have an OnClick event, then it is using an href, so we place this code into the contextAction array. At the same time we add stuff to the contextAction array we also add the text of the action to the labelAction array and we set the flag in the whatAction array to let us know if the action was an onClick or href. Note that I could have used a multidimensional array to store all of these things, but I find it easier to work with multiple single dimension arrays.

Using the example in the database, here's what the arrays would look like if we visualized them as a graphic:



Aside: Different browsers use different function names for the functions that represents certain events. Here's a simple example you can try in your database to see what I mean:

Create a new action button and add the following JavaScript to it:


alert("Here is some simple code");
alert(this.onclick);


When you click this action button on the web, the second dialog box that appears will show you how the browser represents that onClick event. IE and some other browsers use the function name "anonymous", a function that does not define any arguments. Firefox and other browsers use the event name as the name of the function and they use an argument called "event" (which is the Event object that gets passed to an event handler).




Now that you have all of this information within your three arrays, the logic from here on out is dependent on what you want to do. In our case, we want to generate an unordered list. Notice the labels I used for the action buttons include text values separated by an "*". I did this in order to define both the drop-down text and the label for the action within that drop-down menu. Thus, we'll have three drop-down menus in our example, "Demo", "View Actions" and "Create". Making use of this, we're going to iterate through the arrays and throw the menu name into a variable. As we walk the array, if we find that the current label has the same menu name, it will be part of the same <ul>. If it's not the same, we've encountered a new menu and we have to close the existing <ul> and start another one. In either case, we also construct the <li> tag and include the code for it, either using the href value or the onClick event. Please note that in this example, you'll need to keep actions that should be in the same menu grouped together. We could account for items in the same menu being located in different spots on the action bar, but that would make the scripting much more difficult.

Here's the code:


//Generate our unordered lists for use in the CSS Menu

var currentMenu;
var menuButtonText;
var currentMenuLabel;
var menuOutput = '';

//This function is a helper to construct the <li> elements
function makeLink (linkType, linkCode, linkLabel) {
if (linkType == "href") {
menuOutput += "<li><a href=\"" + linkCode + "\">" + linkLabel
+ "</a></li>";
}
else {
menuOutput += "<li><a href=\"#\" onClick=\"" + linkCode + "\">" +
linkLabel + "</a></li>";
}
}

//Iterate through the arrays now
for (i=0; i < contextAction.length; i++) {
menuButtonText = labelAction[i].split('*');
if (menuButtonText[0] == currentMenuLabel) {
//if the menu names are the same, just add an new <li> to the existing <ul>
makeLink(whatAction[i], contextAction[i], menuButtonText[1]);
}
else { //otherwise, create a new menu
currentMenuLabel = menuButtonText[0];
menuOutput += "</ul><li><a class=\"trigger\" href=\"#\">" +
currentMenuLabel + "</a><ul>" //adds the new menu drop down
makeLink(whatAction[i], contextAction[i], menuButtonText[1]);
}
}

//Hides the Domino generated action bar and HR
var actionBarTable = document.getElementsByTagName("Table")[0];
var actionBarHR = document.getElementsByTagName("HR")[0];
actionBarTable.style.display='none';
actionBarHR.style.display='none';

//Write the menu code out to the document
document.write('<div id=\"menuwrapper\"><ul id=\"p7menubar\"><ul>'+ menuOutput +
'<br class=\"clearit\"></ul></li></ul></div>');

//Call P7_ExpMenu() for IE compatibility
P7_ExpMenu();


If you take some time to deconstruct this, you'll find it's not very hard at all. The most difficult part was coming up with the concept.


All of this code lives on a single subform called "CSSMenu" within the example database. If you want to use this in an existing database without knowing anything about how it all works, all you need to do is copy the following design elements into your application:

CSSMenu (subform)
p7exp.css (page)
p7exp.js (page)
p7PM_dark_south.gif (image)

Wherever you position the subform on your main form is where the menu bar will appear. You can leave it at the top of the page, place it within a positioned element, within a table, etc. You'll also need to modify the labels of your action buttons (and maybe their positions) so that they reflect the name of the menu they should go under and the text that should have within that menu (e.g. Rock*Roll). The only other design change is to reference the p7exp CSS file and javascript file in the HTML head of your page.

(The sample database uses the following code in the HTML Head Content, since it uses a conditional comment for making a slight change in the CSS for IE.)


DBPath := @WebDbName;

"<link rel=\"stylesheet\" href=\"p7exp.css\" type=\"text/css\">" + @NewLine +
"<script src=\"p7exp.js\"></SCRIPT>" + @NewLine +
"<!--[if lte IE 7]>" + @NewLine +
"<style>" + @NewLine +
"#menuwrapper, #p7menubar ul a {height: 1%;}" + @NewLine +
"a:active {width: auto;}" + @NewLine +
"</style>" + @NewLine +
"<![endif]-->" + @NewLine +
"<style>body {margin:0; padding:0;font:11pt/1.5 sans-serif;}</style>"


I just tried this. It literally took me 7 minutes to plop the elements into a completely non-web enabled database and have a beautiful looking and nicely functional drop down menu generated from the action bar. Pretty sweet I think!

If you made it this far, then I hope you see the power in using techniques like this to manipulate the DOM. The specifics of this article aren't nearly as important as the underlying idea. Once you know how to access objects via the DOM, the world of dynamic web applications will completely open up to you. I hope that the content here made some sense to you, but if you have any questions, please leave a comment and I'll answer it here.

----

Update: Even before I posted this, I wondered how easy it would be to grab a vertical CSS menuing system from the internet and modify it for use here. Well, it took less than 10 minutes. Since most CSS menus use an unordered list, the only code I had to change was the line that actually writes the menu out to the browser. I also downloaded the CSS and JS files from JavaScript Kit and plopped them in the database. I created a new subform ("VerticalCSSMenu") and just changed the last line. I also made a slight modification to the css to use colors for the menu and hover rather than graphics. So...as you can see, this idea is very easy to extend/change, etc. to meet any needs you might have.



As always, I hope you find this useful. If you come up with other good ideas, please feel free to share. Cheers!

No comments: