A fast and simple async API for Google Mapplets

Michael Geary | Wed, 2007-06-06 16:18

Update 2007-06-22: Version 2 now supports portable code that runs as both a mapplet and a Maps API app. Read about the update.

If you’re building a Google Mapplet that responds to map movement and resizing, you will soon find yourself writing code like this recent gem of mine:

map.getSizeAsync( function( size ) {
    map.getBoundsAsync( function( bounds ) {
        map.getCenterAsync( function( center ) {
            search( size, bounds, center );
        });
    });
});

What is going on here? I have a search() function that takes the current map size, bounds and center, runs a search and displays pins on the map. In a normal Google Maps application I could have simply coded:

search( map.getSize(), map.getBounds(), map.getCenter() );

But a Google Mapplet lives in a strange and different world. To isolate mapplet code from the Google domain, Google runs the mapplet in an IFRAME loaded from the gmodules.com domain. Cross-domain browser security prevents your code from communicating directly with the Google Maps frame loaded from maps.google.com.

The mapplet API uses the iframe fragment hack to allow limited communication between the mapplet and the Google map. This has two consequences:

  • The communication is asynchronous. This doesn’t affect the API for functions that simply set map state—they operate on a “fire and forget” basis. But functions that return information can’t do it directly. You have to provide a callback function that receives the information when it is ready.

  • The communication is slow. Everything is serialized through the fragment identifier (hash) of a hidden IFRAME. The map page and the mapplet frame each have interval timers running to watch for changes to this hash. A single getSomethingAsync() function call requires all these steps:

    1. Mapplet frame sets the hash to represent the function call.
    2. Map page timer wakes up, makes the actual Maps API call, and sets the hash to represent the return value.
    3. Mapplet frame timer wakes up, gets the value from the hash, and calls the callback function.

My code listed above makes three of these round trips to the maps page one after the other, because the callback for each function triggers the next step in the series. That’s a lot of timeouts—enough to cause a noticeable delay.

What if we could somehow combine all three information requests into a single round trip? That should speed things up quite a bit. Imagine a different Mapplet async API where you provide a list of Maps API functions and get back all of their responses in a single callback with multiple arguments. My three nested function calls and callbacks could be reduced to:

GAsync( map, 'getSize', 'getBounds', 'getCenter',
    function( size, bounds, center ) {
        search( size, bounds, center );
    });

(The sharp-eyed reader will note that the search() function could be used directly as the callback because it takes the same arguments:

GAsync( map, 'getSize', 'getBounds', 'getCenter', search );

But we’ll stick with the longer form for this discussion, because it makes it clear what the function arguments are.)

While we’re at it, we can provide a way to retrieve information for more than one object in a single call:

// Get the map center and the location of a marker,
// and find out if the marker is hidden
GAsync(
    map, 'getCenter',
    marker, 'getPoint', 'isHidden'
    function( mapCenter, markerPoint, markerHidden ) {
        // ...
    });

And for functions such as map.fromContainerPixelToLatLngAsync() which take an additional argument, we can allow an optional arguments array after any function name:

// Get the map center, top left corner, and zoom level
GAsync(
    map,
        'getCenter',
        'fromContainerPixelToLatLng', [ new GPoint(0,0) ],
        'getZoom'
    function( center, topleft, zoom ) {
        // ...
    });

Compare that with the equivalent nested functions using the existing API, which would take about three times longer to run:

map.getCenterAsync( function( center ) {
    map.fromContainerPixelToLatLngAsync( new GPoint(0,0), function( topleft ) {
        map.getZoomAsync( function( zoom ) {
            // ...
        });
    });
});

Good news: We don’t have to wait for Google to implement this zippy GAsync API or something like it. Although the public API only exposes individual xyzAsync() functions, the underlying iframe fragment dispatcher can queue up multiple function calls and return values into a single round trip.

The Google Maps team was kind enough to provide me with a nifty makeBarrier() function that allows us to queue up a number of async calls and get a single callback when all their values are ready. Using this function, my first example can be written as:

function makeBarrier( numCalls, callback ) {
    return function() {
        if( ! --numCalls ) callback();
    };
}

var size, bounds, center;

// The 3 here refers to the 3 callbacks that we want to synchronize below
var barrier = makeBarrier( 3, function() {
    search( size, bounds, center );
});

// Fire the 3 callbacks
map.getSizeAsync( function( returnedSize ) {
    size = returnedSize;
    barrier();
});

map.getBoundsAsync( function( returnedBounds ) {
    bounds = returnedBounds;
    barrier();
});

map.getCenterAsync( function( returnedCenter ) {
    center = returnedCenter;
    barrier();
});

As you can see, it’s up to us to count the functions and keep track of the values, but having done that, we can get all three values in a single round trip through the API. It’s literally three times faster than the nested API calls.

Armed with that information, could we code GAsync() as a layer on top of the existing mapplet async APIs? Indeed we can!

You can try out the code right now and see the speed difference with my test mapplet. Go to the Google Maps Developer Preview page, log into your Google account, and click the Add Content link under the Mapplets tab (or click the Browse Content button if that is what is there).

The next page will show a number of existing mapplets. Click the tiny Add by URL link next to the search button at the top of the page, and paste this URL into the URL box that opens up. You can also click this link to see the mapplet source code:

http://mg.to/mapplet/async/async.xml

(When you paste the link, make sure the http:// isn’t duplicated because of the text already in the box.)

Click the Add button and click OK on the confirmation dialog. Then click Back to Google Maps at the top left corner of the page, and you should see a new entry titled A fast simple mapplet async API. Click it to load the mapplet.

An info window should open in the map, displaying several items of information about the map, and the time it required to collect the information using the GAsync() API. Then try the Slow Async API radio button to see the performance using nested async calls.

The GAsync code used in the test mapplet is:

GAsync(
    map, 'getZoom', 'getSize', 'getBounds', 'getCenter',
        'fromContainerPixelToLatLng', [ new GPoint(0,0) ],
    marker, 'getPoint', 'getIcon',
    function info( zoom, size, bounds, center, topleft, point, icon ) {
        // ...
    });

and the corresponding nested async code is:

map.getZoomAsync( function( zoom ) {
    map.getSizeAsync( function( size ) {
        map.getBoundsAsync( function( bounds ) {
            map.getCenterAsync( function( center ) {
                map.fromContainerPixelToLatLngAsync( new GPoint(0,0), function( topleft ) {
                    marker.getPointAsync( function( point ) {
                        marker.getIconAsync( function( icon ) {
                            // ...
                        });
                    });
                });
            });
        });
    });
});
}

Finally, here is the GAsync source code. First, a compact version suitable for pasting into your own mapplet (or download async.js):

// GAsync v2 by Michael Geary
// Commented version and description at:
// http://mg.to/2007/06/22/write-the-same-code-for-google-mapplets-and-maps-api
// Free beer and free speech license. Enjoy!

function GAsync( obj ) {

    function callback() {
        args[nArgs].apply( null, results );
    }

    function queue( iResult, name, next ) {

        function ready( value ) {
            results[iResult] = value;
            if( ! --nCalls )
                callback();
        }

        var a = [];
        if( next.join )
            a = a.concat(next), ++iArg;
        if( mapplet ) {
            a.push( ready );
            obj[ name+'Async' ].apply( obj, a );
        }
        else {
            results[iResult] = obj[name].apply( obj, a );
        }
    }

    var mapplet = ! window.GBrowserIsCompatible;
    var args = arguments, nArgs = args.length - 1;
    var results = [], nCalls = 0;

    for( var iArg = 1;  iArg < nArgs;  ++iArg ) {
        var name = args[iArg];
        if( typeof name == 'object' )
            obj = name;
        else
            queue( nCalls++, name, args[iArg+1] );
    }

    if( ! mapplet )
        callback();
}

And a heavily commented version that explains how it works:

// GAsync v2 by Michael Geary
// http://mg.to/2007/06/22/write-the-same-code-for-google-mapplets-and-maps-api
// Free beer and free speech license. Enjoy!
//
// Call one or more xyzAsync() functions from the Google
// Mapplet API, with a single callback that receives the
// values from all of the called functions. The first argument
// to GAsync is the object to be used. The next arguments
// are each of the function names, without the 'Async'
// suffix. The last argument is the callback function.
// To call xyzAsync() functions for more than one object,
// list another object in the argument list and the function
// names after that will use that object.
// To call an xyzAsync() function that takes arguments of
// its own (other than the callback argument), place an
// array of those arguments after the function name in
// GAsync's argument list.

// Example calls, given existing map and marker objects
/*
    // Get the size, bounds, and center of the map
    GAsync( map, 'getSize', 'getBounds', 'getCenter',
        function( size, bounds, center ) {
            // ...
        });

    // Get the zoom level, size, bounds, and center for
    // the map, as well as the lat/long of the top left
    // corner of the map. Also get the point and icon
    // for marker.
    GAsync(
        map, 'getZoom', 'getSize', 'getBounds', 'getCenter',
            'fromContainerPixelToLatLng', [ new GPoint(0,0) ],
        marker, 'getPoint', 'getIcon',
        function( zoom, size, bounds, center, topleft, point, icon ) {
            // ...
        });

    // Equivalent code using nested xyzAsync calls:
    map.getSizeAsync( function( size ) {
        map.getBoundsAsync( function( bounds ) {
            map.getCenterAsync( function( center ) {
                // ...
            });
        });
    });

    map.getZoomAsync( function( zoom ) {
        map.getSizeAsync( function( size ) {
            map.getBoundsAsync( function( bounds ) {
                map.getCenterAsync( function( center ) {
                    map.fromContainerPixelToLatLngAsync( new GPoint(0,0), function( topleft ) {
                        marker.getPointAsync( function( point ) {
                            marker.getIconAsync( function( icon ) {
                                // ...
                            });
                        });
                    });
                });
            });
        });
    });
*/

function GAsync( obj ) {

    // Call the callback function provided in the GAsync() call
    function callback() {
        args[nArgs].apply( null, results );
    }

    // Queue a single xyzAsync() function call.
    // 'iResult' is the index into the final results array.
    // 'name' is the name of the Maps API function without
    // the Async suffix.
    // 'next' is the next argument to GAsync following name;
    // If next is an array, it contains the arguments to be
    // passed to xyzAsync().
    function queue( iResult, name, next ) {

        // Callback for the xyzAsync() function that was called
        // by this invocation of the queue() function.
        // value is the return value from the Maps API.
        function ready( value ) {

            // Save the result in the final results array
            results[iResult] = value;

            // If every async function has completed, call the
            // GAsync callback (in the last argument to GAsync)
            // with the final results array as its arguments.
            if( ! --nCalls )
                callback();
        }

        // Arguments array for the xyzAsync() call
        var a = [];

        // If 'next' is an array, it contains arguments to
        // be passed to xyzAsync()
        if( next.join )  // Arrays have .join, strings do not
            a = a.concat(next), ++iArg;  // append and skip

        // Call xyzAsync() in a mapplet or xyz() in the Maps API
        if( mapplet ) {
            // The callback for xyzAsync is its last argument
            a.push( ready );

            // Call xyzAsync() with arguments in 'a'
            obj[ name+'Async' ].apply( obj, a );
        }
        else {
            // Maps API, call xyz() and save its return value
            results[iResult] = obj[name].apply( obj, a );
        }
    }

    // Is this is a mapplet or the Maps API?
    var mapplet = ! window.GBrowserIsCompatible;

    // 'args' is a reference to GAsync's arguments array
    // that can be used in the nested functions.
    // 'nArgs' is is the number of arguments, not counting
    // the callback function at the end. (Thus, args[nArgs]
    // is a reference to the callback function.)
    var args = arguments, nArgs = args.length - 1;

    // 'results' is the final results array for the callback. It
    // will be populated from the individual ready() callbacks.
    // 'nCalls' is the total number of xyzAsync() calls. It is
    // incremented as those calls are queued, and then
    // decremented to discover when all the calls are done.
    var results = [], nCalls = 0;

    // Loop through GAsync()'s arguments, starting at
    // the first function name (after 'obj'), and ending
    // before the callback function.
    for( var iArg = 1;  iArg < nArgs;  ++iArg ) {

        // Get the name of the function to be called
        var name = args[iArg];

        // If the argument is an object, not a name,
        // switch to that object for subsequent calls.
        // If it is a name, count and queue the function.
        if( typeof name == 'object' )
            obj = name// change object
        else
            queue( nCalls++, name, args[iArg+1] );
    }

    // If using the Maps API, call the callback now
    if( ! mapplet )
        callback();
}

Of course, there are still cases where you will have to run one async call after another one. If you need one piece of information as input to a subsequent call, nested async functions are the way to do it. Even then, it may be possible to combine some other async calls into a single GAsync call, wherever they don’t depend on each other’s results. You’ll shave about a quarter second off your mapplet’s response time for every call you combine using GAsync.

Enjoy your simpler and faster mapplet code!

Thanks to Ben A. of Google for mapplet design and coding tips.

An email panic button

Michael Geary | Tue, 2007-04-10 08:31
category

Today’s USA Today has an interview with David Shipley and Will Schwalbe, the authors of Send: The Essential Guide to Email for Office and Home. The article has some good tips, but one remark caught my eye:

They say one thing everyone wants, but no one has invented, is a “panic button,” a short delay after hitting send, like the kind TV networks use to bleep obscenities.

If you’re using Microsoft Outlook, it’s easy to set up a sending delay. I’ve been using one for years. I don’t usually put obscenities in my emails, but it sure is handy for the many times that I’ve forgotten to include an attachment and remembered right after hitting Send.

The feature is fairly well hidden, so Shipley and Schwalbe can be forgiven for not knowing about it. Here is how to set it up in Outlook 2003. The exact steps will vary in other versions of Outlook, but it should be similar.

  1. Starting on the main Outlook window, open the Tools menu and select Rules and Alerts…
  2. Click the New Rule… button.
  3. Click the Start from a blank rule radio button.
  4. Under Step 1, click Check messages after sending.
  5. Click Next >
  6. Don’t select any of the conditions in the list. Click Next >
  7. A message box will warn that “This rule will be aplied to every message you send. Is this correct?” Click Yes.
  8. Click Next >
  9. Under Step 1, click the defer delivery by a number of minutes checkbox.
  10. Under Step 2, click a number of.
  11. A dialog titled Deferred Delivery will open. Choose the number of minutes you want to delay outgoing email and click OK.
  12. Click Next >
  13. Don’t select any of the exceptions in the list. Click Next >
  14. Enter a name for the rule, such as Defer Sending.
  15. Click Finish.

With this rule enabled, when you send a message, it will remain in Outlook’s Outbox for the number of minutes you specified. You can open the Outbox, open your message, and edit it before sending again. The same time delay will apply again.

Beware of one annoying Outlook bug: Depending on what add-ons are installed in Outlook, you may find that after you edit a message and re-send it, it remains in the Outbox and does not get sent at all. You’ll notice that the message in the Outbox list was italicized before you re-edited it, but after sending it the second time it is no longer in italics. This is Outlook’s subtle hint that it is not going to send the message at all!

To work around this, open the message from the Outbox, and before you re-send it, select any other folder in the main Outlook window. Then you can send the message and it will go out as expected. (If you’re curious, after resending the message, you can check the Outbox again and you’ll see that it is italicized, indicating that Outlook will send it after all.)

JKLmouse: the automatic keyboard mouse for notebook computers

Michael Geary | Sun, 2007-03-04 08:19

Have you ever wanted to use the mouse to move something with pixel perfect precision? There is a way to do it: a “keyboard mouse.” One good one comes with Windows, called MouseKeys. If you’re using a desktop computer with a dedicated numeric keypad, you can turn on MouseKeys and leave it on, then use the numeric pad to move the mouse in any direction one pixel at a time.

Unfortunately, MouseKeys is barely usable on a notebook computer. You can get it to work, but you’ll have to turn it on and off all the time because it prevents normal use of the keyboard—the “numeric pad” on my ThinkPads and on most other notebooks is overlaid on the QWERTY keyboard.

So I wrote JKLmouse, the automatic keyboard mouse for notebook and laptop computers. JKLmouse doesn’t use any special modes—it is always active—and it doesn’t interfere with normal use of the keyboard.

The secret is simple: JKLmouse turns keyboard keys into mouse movement keys only while a mouse button is held down. When no mouse button is down, the keyboard works normally. When any mouse button is down, you can use the cursor arrow keys to move the mouse, and you can also use keys on and around the home row: JKL for the right hand or SDF for the left, along with the keys above and below those.

This works especially well on a ThinkPad, where the TrackPoint is also right next to the home row. You can start moving something with the TrackPoint, then continue to hold the mouse button down and use JKL and nearby keys to move the last few pixels, one pixel at a time. There’s no special “keyboard mouse” mode—you can combine TrackPoint and keyboard movement seamlessly. It also works fine with touchpads—just keep the mouse button down and you can use either the touchpad or the JKLmouse keys.

What if you want to use JKLmouse without holding a mouse key down? Hold down the Caps Lock key and you can use all of the JKLmouse keys while Caps Lock is held down. This won’t turn on the Caps Lock mode and doesn’t interfere with normal use of Caps Lock.

I wrote JKLmouse for my own use because I wanted a keyboard mouse that Just Works on my ThinkPad, without the special modes that other keyboard mice use. I’ve found it tremendously useful and I hope you enjoy it too.

JKLmouse runs on Windows 95, 98, 98SE, Me, 2000, and XP. I haven’t tested it on Windows Vista yet. See the download page for more details:

www.jklmouse.com

Enjoy!

We are not evil, and we will burn down your church to prove it

Michael Geary | Sat, 2006-09-16 11:44
category

I’m a big fan of The Daily WTF, even if I can’t explain its name in mixed company. But today the real world has outdone anything that site ever posted.

You see, the Pope gave a speech that quoted a 14th century Byzantine emperor who said that Mohammed’s teachings were “evil and inuhuman”. Naturally, some people took offense. No one likes to have their faith criticized, and a few people decided to prove that emperor wrong. To demonstrate their lack of evil, they burned and shot up a few churches.

WTF?

Seth Godin, read "I, Pencil"

Michael Geary | Sat, 2006-07-22 22:26
category

Seth Godin is one of the smartest marketing guys around. But he seems misinformed about economics.

In his article No stoplights, Seth says:

While individuals might moan about how they were treated, we all realize that without some sort of central allocation of scarce resources (like a piece of tarmac or a booth at a trade show), chaos ensues. And the chaos hurts everyone.

Well, no. Consider the lowly pencil. Cheap, effective, ubiquitous. But nobody knows how to make one. Nobody.

There is no one person on Earth who knows how to find, process, and assemble all of the materials that go into a pencil. A lot of people know their own parts of the puzzle, but nobody knows the whole thing.

No central allocator. But somehow pencils get made, and plenty of them.

Leonard Read can explain it better than I can:

I, Pencil

Now, if nobody knows how to make a pencil, how could anyone centrally allocate all of the scarce resources needed to make one? How in the world would they know what to allocate?

And watch out. If you do get that central allocator, it will turn out to be somebody who believes:

A pencil factory is not a big truck. It’s a series of tubes.

And if you don’t understand those tubes can be filled and if they are filled, when you put your lead in, it gets in line and its going to be delayed by anyone that puts into that tube enormous amounts of material, enormous amounts of material…

Prince Tu'ipelehake and Princess Kaimana, R.I.P.

Michael Geary | Fri, 2006-07-07 20:07
category

When I tell people the address of my blog, they sometimes ask me, “mg.to? Huh? What’s .to?” I explain to them that it is the country code domain for the Kingdom of Tonga, but .to names are open to anyone, and I registered the name because it was fun to have such a short domain name with my initials in it.

Even with that small connection to Tonga, I was deeply saddened to read of the deaths of Prince Tu’ipelehake and Princess Kaimana and Vinisia Hefa. Prince Tu’ipelehake was on a tour speaking at several Tongan churches in the Bay Area, when a teenage driver who was racing on Highway 101 hit their Ford Explorer, which then overturned.

My prayers and condolences to the royal family and the Tongan people.

Prototype vs. Web 2.0

Michael Geary | Sat, 2006-05-20 08:21
category

In yesterday’s Ajaxian, Rob Sanheim reviews a post that explains why programmers should not use JavaScript’s Array type for associative arrays. Array objects should be used only for integer-indexed arrays. The correct type for an associative array in JavaScript is Object, not Array.

Rob is right, of course. A JavaScript Object is an associative array or hash table. An Array is also an Object (in fact, typeof [] == 'object'), but it’s really meant for arrays indexed by integers.

It’s an easy mistake to make, especially for someone who has come from a language where the associative array type is called an array. I don’t think I’ve made this particular mistake, but I have made a similar one. I used to use the for-in loop because I liked its simplicity:

var a = [ 'a', 'b', 'c' ];
for( var i in a )
   alert( a[i] )

That looks better to me than the C-style loop:

var a = [ 'a', 'b', 'c' ];
for( var i = 0;  i < a.length;  i++ )
   alert( a[i] )

The for-in loop works on an Array because an Array is an Object, but that also means it treats the Array as an Object. It enumerates the array elements because they are also properties of the underlying Object, but it doesn’t necessarily enumerate them in order by index:

var a = [];
a[1] = 'b';
a[2] = 'c';
a[0] = 'a';
for( var i in a )
   alert( a[i] )

In the browsers I tried, this code alerts the letters in the order that they were added to the array, not in index order. But there’s no guarantee about that order either, because the for-in loop doesn’t promise any particular order of enumeration.

Worse, for-in also enumerates any extra properties or methods added to the array:

var a = [ 'a', 'b', 'c' ];
a.alert = function() {
   for( var i in this )
      alert( a[i] );
}
a.alert();

This alerts the source code for the alert method as well as the three letters in the array, because they are all properties of the underlying Object.

The same thing happens if the alert method is added via Array.prototype:

var a = [ 'a', 'b', 'c' ];
Array.prototype.alert = function() {
   for( var i in this )
      alert( a[i] );
}
a.alert();

And that’s where we get into trouble with Prototype, because it adds methods to Array.prototype. This breaks any code that uses for-in on an Array. But as we just discussed, no one should do that, so it won’t be a problem in any properly-written code. Right? Case closed.

Rob puts it more strongly:

The other problem with using Arrays as associative arrays means you can no longer extend Array.prototype, which is what Prototype 1.5 does to great effect. Prototype did break Object associative arrays in 1.4 with additions to Object.prototype, something that is fixed in 1.5 after much wailing and gnashing of teeth. Some might argue extending any of the built-in objects’ prototypes is bad form, but those people are wrong.

I wouldn’t argue that it’s bad form, but extending the native object prototypes causes real pain for two groups of people:

  • People who write JavaScript widgets to be used on other websites, and

  • People who use third-party JavaScript widgets on their sites.

It’s that whole Web 2.0 mashup thing, you know?

As a widget author, if I use Prototype, then sooner or later one of my customers will want to combine my widgets with JavaScript code from somebody who does use a for-in loop on an array—“correct” or not. Or code that uses some other library that also extends Array.prototype—even a different version of Prototype that conflicts with mine.

I love the good taste of syntactic sugar as much as anyone, and if it didn’t break other code I’d be merrily adding methods to the native objects too. But I have to make my code compatible not only with the code I control, but the code I don’t. So it’s no Prototype for me.

Best news interview ever

Michael Geary | Sun, 2006-05-14 21:43
category

Want to know the significance of the Apple vs. Apple ruling?

Ask a cabbie.

Is Jesus the next killer app?

Michael Geary | Sat, 2006-04-29 17:24
category

Your <body> is in your <head>

Michael Geary | Fri, 2006-04-28 19:16

I was chasing down a bizarre bug. My JavaScript code was working fine in IE, Firefox, and Safari, until I tried using it here on mg.to. It still worked in Firefox and Safari, but it blew up in IE, with behavior that made no sense at all. I was seeing duplicate copies of DOM elements I created using my DOM creation plugin for jQuery, and all kinds of strange behavior. It was almost as if something was fundamentally wrong with the way the DOM was working.

I’m usually pretty good at tracking down problems, but this one had me stumped. I wanted to look around the the DOM structure, but since this was IE, I couldn’t use any of the usual Firefox plugins such as the DOM Inspector or FireBug. Then I found a great tool for IE troubleshooting, the DebugBar.

I looked at the DOM with the DebugBar, and my jaw dropped when I saw this:

DebugBar DOM

(This screen shot and the others below are from simplified test cases.)

This had to be impossible! The document <body> was inside the <base> tag, which in turn was inside the <head>. The latter I’d expect, but why was <body> not a sibling of <head> as it should be? And how was <base> involved in this?

I got lucky with some searches and found these articles by Justin Rogers from the IE team:

This part of the “Implied tags” article seemed to explain what was going on:

The set of implied rules has impacts in other areas as well. You can, for instance, end up using document.writeln to prematurely terminate your HEAD element and move a bunch of stuff out into the BODY. So, if you are doing inline document writes you should probably do them where you want the content to go. Writing the content out in script blocks that appear in the head is the wrong way to go about it. You could hook up to some events or have a container element that you write into, and that is acceptable, but with inline writes you could get unexpected behavior.

Recently I noticed a site that was doing a document.writeln in their HEAD element about half-way through the head content. End result? Well, the content got moved into the BODY element and the object model tree for the page was completely wrong. Good thing they weren’t navigating the object model looking for stuff and good thing the extra META/LINK elements weren’t being used as well. With a static parse of the page you wouldn’t even notice these problems, but when DHTML becomes involved it can change the structure of your document on the fly and rewrite what the object model tree looks like.

Indeed, this site runs on Drupal, and Drupal 4.6.x does use a <base> tag. (The forthcoming Drupal 4.7 eliminates the <base> tag.)

Justin’s examples showed an unclosed <base> tag like this:

<base href="foo">

That should be OK; the W3C HTML specification defines the BASE element as EMPTY, so it shouldn’t require any kind of closing tag (except for XHTML compatibility).

The <base> tag that Drupal generates is self-closing in the XHTML style:

<base href="http://mg.to/" />

However, IE6 does not seem to recognize that the tag is closed (or EMPTY), and it puts everything after that inside the BASE element.

(You may also notice that strictly speaking, Drupal’s <base> tag is incorrect. It should include a filename, but it seems to work OK without it—except for the IE problem.)

On a hunch, I tried closing the tag the old fashioned way:

<base href="http://mg.to/"></base>

and presto! Everything started working, and DebugBar revealed that the <head> and <body> elements were siblings, both direct children of <html> as expected.

The bottom line: Every HTML document in the world that uses a <base> tag is being parsed in this odd way by IE, unless an explicit closing </base> tag is used. It doesn’t affect ordinary HTML rendering, but any kind of DOM manipulation may go haywire.

Here are the three test cases. First, the unclosed/empty <base> tag:

Unclosed BASE tag

The XHTML-style <base /> tag is no better:

Self-closing BASE tag

And the one that works, with a closing </base> tag:

BASE tag with closing tag

There is one remaining problem. If you validate your pages as HTML 4.01 Transitional, there is no way to use a <base> tag that works correctly in IE and also validates. The validator barfs on the closing </base> tag, because it figures the tag is already closed (being an EMPTY element).

If you use XHTML 1.0 (either Transitional or Strict), then you can use the closing </base> tag and it will validate. Since most people who validate their pages are probably using XHTML anyway, this shouldn’t be a problem for many.

However, the W3C’s XHTML/HTML Compatibility Guidelines offer this warning:

…use the minimized tag syntax for empty elements, e.g. <br />, as the alternative syntax <br></br> allowed by XML gives uncertain results in many existing user agents.

Well, that’s just great. The only syntax that works in IE and validates is <base></base>, but W3C warns against it. I haven’t actually seen any problems caused by using this syntax with the <base> tag, though, even in old browsers. So for now, I’m using it and hoping for the best.

Major thanks are due to the DebugBar for pointing me toward the problem, and Justin Rogers for explaining it.

Google Romance uses ThinkPad X41 Tablet

Michael Geary | Sat, 2006-04-01 13:57
category

Feeling lucky?

Thanks to Google’s new Contextual Dating service, the ThinkPad X41 Tablet PC can help!

X41 Tablet at your service

DOM creation: good, bad, and ugly

Michael Geary | Wed, 2006-03-29 23:05
category

The Ajaxians are talking about a new $E function that is supposed to make it easier to create DOM elements. The example given creates the equivalent of this HTML code:

<div id="toolGroup_1" class="toolGroup">
   <div class="roundBarTop">
      <div class="leftEdge"></div>
      <div class="rightEdge"></div>
      <div class="heading">
         <a class="collapser"></a>
         Group Heading
      </div>
   </div>
</div>

using this code to do it:

var element=$E({
   tag:'div',
   className:'toolGroup',
   id:'toolGroup_1',
   children:{
      tag:'div',
      className:'roundBarTop',
      children:[{
         tag:'div',
         className:'leftEdge'
      },{
         tag:'div',
         className:'rightEdge'
      },{
         tag:'div',
         className:'heading',
         children:[{
            tag:'a',
            className:'collapser'
         },
            'Group Heading'
         ]
      }]
   }
});

That seems just a tad complicated! Let’s see how it would look using my DOM creator for jQuery and Prototype:

var element =
   $.DIV({ Class:'toolGroup', id:'toolGroup_1' },
      $.DIV({ Class:'roundBarTop' },
         $.DIV({ Class:'leftEdge' }),
         $.DIV({ Class:'rightEdge' }),
         $.DIV({ Class:'heading' },
            $.A({ Class:'collapser' }),
            'Group Heading'
         )
      )
   );

Ah, that is quite a bit simpler. It’s also more flexible. Suppose you want to save a reference to that A tag in the middle. There’s no way to do that with $E, but it can be as easy as this:

var atag;
var element =
   $.DIV({ Class:'toolGroup', id:'toolGroup_1' },
      $.DIV({ Class:'roundBarTop' },
         $.DIV({ Class:'leftEdge' }),
         $.DIV({ Class:'rightEdge' }),
         $.DIV({ Class:'heading' },
            atag = $.A({ Class:'collapser' }),
            'Group Heading'
         )
      )
   );

San Jose Snow

Michael Geary | Fri, 2006-03-10 13:32
category

The view from my balcony this morning. Do you see the birds?

Snow on the hills east of San Jose

Easy DOM creation for jQuery and Prototype

Michael Geary | Mon, 2006-02-27 11:07

Here is a jQuery plugin that makes it easy to build up a tree of DOM nodes. It lets you write code like this:

var table =
   $.TABLE({ Class:"MyTable" },
      $.TBODY({},
         $.TR({ Class:"MyTableRow" },
            $.TD({ Class:"MyTableCol1" }, 'howdy' ),
            $.TD({ Class:"MyTableCol2" },
               'Link: ',
               $.A({ Class:"MyLink", href:"http://www.example.com" },
                  'example.com'
               )
            )
         )
      )
   );

Basically, each function such as $.TABLE creates a DOM node and takes the following arguments:

The first argument is an object that list any attributes to be set on the node. You can specify the className attribute in a few different ways depending on your taste:

  className: text
  Class: text
  'class': text

Any additional arguments after the first one represent child nodes to be created and appended. These arguments can be DOM elements themselves (e.g. inline $.FOO calls as above), or they can be numbers or strings which are converted to text nodes, or they can be arrays, in which case each element of the array is handled in this same way.

This interface is inspired by Bob Ippolito’s MochiKit DOM API, although it doesn’t implement all of the features of that one (yet).

The code predefines most of the common tags; you can add additional tags by calling:

$.defineTag( tagName )// e.g. $.defineTag( 'dd' );
 

Or simply add the tag names to the tags list in the code.

One last definition is $.NBSP which defines a non-breaking space (same as &nbsp; in HTML).

$._createNode is an internal helper function used by the $.FOO functions. I would have hidden it away as a nested function, but I wanted to avoid any unnecessary closures.

This code doesn’t actually depend on any of the features of jQuery except for the presence of the $ function—and it uses $ only as a way to avoid cluttering the global namespace. I haven’t tested it with Prototype.js, but it should work equally well there. Or the code can be used with no library, by preceding it with:

var $ = {};

Here is the source code, or you can download it:

// DOM element creator for jQuery and Prototype by Michael Geary
// http://mg.to/topics/programming/javascript/jquery
// Inspired by MochiKit.DOM by Bob Ippolito
// Free beer and free speech. Enjoy!

$.defineTag = function( tag ) {
    $[tag.toUpperCase()] = function() {
        return $._createNode( tag, arguments );
    }
};

(function() {
    var tags = [
        'a', 'br', 'button', 'canvas', 'div', 'fieldset', 'form',
        'h1', 'h2', 'h3', 'hr', 'img', 'input', 'label', 'legend',
        'li', 'ol', 'optgroup', 'option', 'p', 'pre', 'select',
        'span', 'strong', 'table', 'tbody', 'td', 'textarea',
        'tfoot', 'th', 'thead', 'tr', 'tt', 'ul' ];
    for( var i = tags.length - 1;  i >= 0;  i-- ) {
        $.defineTag( tags[i] );
    }
})();

$.NBSP = '\u00a0';

$._createNode = function( tag, args ) {
    var fix = { 'class':'className', 'Class':'className' };
    var e;
    try {
        var attrs = args[0] || {};
        e = document.createElement( tag );
        for( var attr in attrs ) {
            var a = fix[attr] || attr;
            e[a] = attrs[attr];
        }
        for( var i = 1;  i < args.length;  i++ ) {
            var arg = args[i];
            if( arg == null ) continue;
            if( arg.constructor != Array ) append( arg );
            else for( var j = 0;  j < arg.length;  j++ )
                append( arg[j] );
        }
    }
    catch( ex ) {
        alert( 'Cannot create <' + tag + '> element:\n' +
            args.toSource() + '\n' + args );
        e = null;
    }

    function append( arg ) {
        if( arg == null ) return;
        var c = arg.constructor;
        switch( typeof arg ) {
            case 'number': arg = '' + arg;  // fall through
            case 'string': arg = document.createTextNode( arg );
        }
        e.appendChild( arg );
    }

    return e;
};

Coolest satellite ever

Michael Geary | Thu, 2006-01-26 15:09
category