with Ges Seger

STRAAANGE COOODE

Today's Episode:

Combo Number 5

I was a little miffed when I came into work that morning. Most of the previous day had been spent loping on a treadmill wired to a telemetry harness while the local medical profession did their level best to induce a heart attack. Not only that, I had received two injections of radioactive Thallium during the course of that exam and still hadn't developed super-powers twenty-four hours later. On the bright side, the exam had made me miss a peer review of the latest module I was coding for the Application Which Must Be Web Enabled™. I printed out a copy of the meeting minutes and started reading about what they had managed to break over my morning coffee. Let's see now...

At that last item, the mouthful of coffee I was about to swallow teleported out of my mouth. Fortunately, my mug was there to intercept it before rematerialization could occur within my keyboard. The section of HTML in question had been developed a few Strange Codes ago, and had been done in response to what I had originally thought was egregiously bad design of the application's previous version. Upon further questioning, I discovered that what the reviewers were asking for was a feature of that version I had never discovered during my initial design workup. Instead of the two textfields per row of data I had assumed they were using, the original programmers had rigged up a sort of combo box in each row similar to the widget used in many Windows applications.

Now they wanted me to implement that in HTML.

The previous day's cardiac stress test was suddenly looking more attractive as a way to spend the business day.

SOLUTION

The first question I asked myself was "What HTML tags could be used to emulate a combo box?" The short answer:

The <SELECT> tag part of the combo element will normally be invisible. If the user starts typing text into the <INPUT> field, the <SELECT>; tag will appear and start scrolling toward the entry they want. Alternatively, the user can click the <INPUT> to make the <SELECT> tag appear or disappear.

At this point, I had a forehead-smacking epiphany. In the module which I was currently programming, I had been inserting the same menu again and again and again for each row of output being slammed out to the browser. Since said menu typically ran several hundred entries, you can just imagine the size of the resulting page being generated by the server. I realized at this point that if I was going to combo elements for each row instead of popup menus, I didn't need to put the menu in each and every row. I only needed to put it into the page once and let the Javascript I was writing position it where it was needed, when it was needed. I could almost hear the server thanking me for lessening its load.

Now that I knew what I was shooting for, I started slinging code. A couple of days later, I still hadn't developed super-powers, but had the next best thing. I had my DHTML-based combo box.

WALKTHROUGH

Let's go to the code -- all of it. Since there's so many little functions and so much happening, I'm not running each snippet through my usual perl one-liner that adds line numbers.

Let's first look at the HTML markup we're using for our combo element:

    <input type="text"
           name="test1"
           value=""
           onclick="toggle(this, 'mst3k')"
           onkeyup="searchList(this, 'mst3k')">
    

Nothing sophisticated is happening here other than the event handlers we're defining for our combo element. To correctly simulate the Windows combo, we'll need to trap the click event (to toggle display of the popup menu) and the keyup event (to change the selected item of the menu to match with what the luser is typing into the input field).

    <select class="combo_menu" name="mst3k" size=5 onclick="searchDone(this)">
      <option>Joel</option>
      <option>Mike</option>
      <option>Cambot</option>
      <option>Gypsy</option>
      <option>Tom Servo</option>
      <option>Crow</option>
      <option>Dr. Forrester</option>
      <option>Dr. Erhardt</option>
      <option>TV's Frank</option>
    </select>
    

I define the popup list used by all combo elements at the bottom of the form, but its positioning within the form isn't critical. What is critical is that this list is defined as absolutely positioned on the page. This is the only way I could get the javascript functions described below to manage its position on the page.

Now onto the Javascripten we'll need:

      var buffer, menu;

      // -- Determine browser
      var IE  = (document.all)? true: false;
    

Before we define any functions, we need to declare some variables they will all use. buffer will store a reference to the input-field part of the active combo form element. The variable IE will be used to determine how to position the menu once it's visible.

      function toggle(ref, m) {
        var menu = eval('ref.form.' + m);
        if (menu.style.display == 'block') {
          menu.style.display = 'none';
          return;
        }

        // -- if we're here, relocate the menu object and make it visible
        var r = getRect(ref);
        with (menu.style) {
          left    = r.left + "px";
          top     = r.bottom + "px";
          display = 'block';
        }

        // -- wouldn't hurt to save a reference to the active combo element
        buffer = ref;
      }
    

This code is responsible for handling the visibility and position of the popup list. The initial block checks to see if the popup is already visible. If it is, we change the display property so it isn't and bail out. I use display rather than visibility for much the same reason I use it in my Strange Code on Trees -- I don't want to use up page real estate when it's invisible.

That was the easy part. The rest of the code not only makes the popup visible, it moves it to the vicinity of the combo element we are attempting to fill in. getRect is a function that I originally developed for a sequel to the Drag & Drop episode, coming to a Browser Near You real soon now. Upon reviewing the first draft of this episode after testing the sequel, I realized that using getRect here would save me both a initialization routine on page load and some incredible contortions I was going through to determine my page position. I'll explain getRect below, but suffice to say that it returns the top, bottom, right, and left positions of the input HTML element. We set the left side of the popup equal to the left side of the source field, and then set the top of the popup equal to the bottom of the source field. That way, the menu appears directly below the source.

As a last act, we cache a reference to our target input field. We don't need it now, but trust me when I say we will need it.

      function searchList(ref, m) {
        var i;
        var menu = eval('ref.form.' + m);

        // -- decloak if not already visible
        if (menu.style.display == 'none')
          toggle(ref, m);

        // -- loop over all options until we get a match
        for(i=0; i<menu.length; i++) {
          var opt = menu.options[i];
          if (ref.value != opt.text.substr(0, buffer.value.length))
            continue;
          opt.selected = true;
          break;
        }
      }
    

Finally we're at the money code. This function takes the currently-entered value of the combo element's input field and then loops over all the list options until it finds a match against an option's text property. If the popup list isn't already visible, it calls the toggle function to make it so.

Pay special attention to how I match the list option with the text field. Rather than looking at the whole string or evaling stuff together to form a regular expression, I only look at the first n characters of the list option's text property -- n being the current length of the value in the text field. The code was far, far easier to write by judiciously using the substr method than by any other method.

      function searchDone(menu) {
        buffer.value = menu.options[menu.selectedIndex].text;
        menu.style.display = 'none';
      }
    

This routine is bound to the popup menu as described in the USAGE section below, and fires when the luser finally makes his selection from the menu. We write the value of the menu choice's text property to the value property of the target input field previously saved in the buffer variable, then set the display property to 'none'.

Please note that if you need any other code to take action on your menu choice, such as my regular-expression tricks from a few articles ago, this is the routine in which to insert it.

Introducing getRect

Starting with IE/Win 5.5, Microsoft provides a function called getClientBoundingRect which returns an object containing the input object's position in pixels for each of its sides. That's the good news. The bad news far outweighs the good:

This last point was the showstopper that forced me to reimplement getClientBoundingRect in a cross-browser, page-relative manner. The general idea of getRect is to walk up the DOM node tree from the starting element, adding our position offsets to our return object properties as we go. The structure of this object will precisely resemble that generated by getClientBoundingRect.

 0      function getRect(obj) {
 1        var rect = new Object();
 2        rect.top = rect.left = 0;
 3        var parentObj = obj;
    

We have to have numerical values to start our sums, so we spend line 2 doing just that to rect.top and rect.left. Line 3 declares and initializes the variable that will reference our current level in the tree.

 4        while (parentObj != null) {
 5          rect.top  += parentObj.offsetTop;
 6          rect.left += parentObj.offsetLeft;
 7          parentObj = parentObj.offsetParent;
 8        }
    

Line 4 starts our climb up the DOM node tree. Lines 5 and 6 add the offset of the current positioning context to the running totals being maintained for the object's left and top sides. Line 7 is the most important line, for we attempt to access this element's parent and store it to our parentObj variable. It's important that we use the offsetParent property instead of the parentElement property, for we want the next node above this one that influences position context in display space. With this done, we loop back to line 4 and make sure there's actually a node waiting for us at the next level. The moment there isn't (parentObject returns null), we fall out of the top of the DOM node tree.

 9        rect.bottom = rect.top  + obj.offsetHeight;
10        rect.right  = rect.left + obj.offsetWidth;
11        return rect;
    

Now that we have a good position for the upper left-hand corner of our target element, we can calculate the other two coordinates of our rect object. Once that's done, we can return the position object to the calling routine.

USAGE

A bit of hard-wiring is required to get everything working correctly:

If that seems like a little much to handle, just View Source on this page and you'll see exactly how everything is supposed to work together.

Also, please note that because of parameters I pass each of these functions, I can use these scripts for more than one unique combo box per page. This had been a limitation of my first draft, so you can imagine how relieved I was to delete that paragraph from the next section of this episode.

BUGS FEATURES

The menu position in IE/Mac is a little off only because I have taken no account of its idiosyncrasies in getRect.

Yes, I've seen the song parody/animation of the same name as this episode. Where do you think I got the idea for the title?

I hardly watched Mystery Science Theater 3000 after the sixth season (mostly because by then I had children old enough to wonder what Mommy and Daddy were watching that made them laugh so hard). If the list of regular characters in the example below seems a little on the small side, that's why.

EXAMPLE

The Satellite of Love Demonstration Form

Link MST3K Character
Tom Servo
Gypsy