2008-10-01

Altering Map Links

While playing with Wufoo, I was impressed by how addresses automatically include a link to map it. Very convenient. It would be even more convenient if you could choose who it maps with, rather than being stuck with Yahoo.

Sadly, that option does not exist. So I decided to write my first Greasemonkey script, and make that option myself. This entailed re-learning javascript and the DOM, and the unique properties of Greasemonkey scripts. Here was my first attempt, adapted from an example in the free online book Dive Into Greasemonkey:

var allElements, thisElement;
anchors = document.evaluate(
  "//a[@title='Show a Map of this Location']",
  document,
  null,
  XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
  null
);

for (var i = 0; i < anchors.snapshotLength; i++) {
  link = anchors.snapshotItem(i);
  link.href.match(/addr=([^&]+)\&csz=(.+)/);
  link.href = "http://maps.google.com/q?" + RegExp.$1 + ", " + RegExp.$2;
}
My first naive attempt failed because Wufoo generates links on the fly... this anchor in question does not exist yet when this script executes, just after the page finishes loading. So I needed to instead check the link right when I clicked on it. Not too difficult to adapt a different example:
document.addEventListener('click', function(event) {
  if (event.target.title == 'Show a Map of this Location') {
    event.target.href.match(/addr=([^&]+)\&csz=([^&]+)/);
    event.target.href = "http://maps.google.com/maps?q=" + RegExp.$1 + ", " + RegExp.$2;
  }
}, true);

A lot simpler! But still wrong. As an experienced javascript code monkey would know, the click event returns the most specific element it can. In the case of Wufoo's address, it returns the span containing the specific address element... <span class='region'>, or <span class='zip'>, etc. Depending on where the user clicks, they MIGHT get the anchor itself, or one of its child elements. So I need to search from the given element up until I find the anchor before altering.

The most appropriate method is to do a recursive search up. This unfortunately required me to create a global function on the window object... not very pretty, but it works:

window.myrddinFindParent = function(element,elementType) {
  if (element.tagName.toLowerCase() == elementType.toLowerCase()) {
    return element;
  } else {
    if (element.parentNode != null) {
      return window.myrddinFindParent(element.parentNode, elementType);
    } else {
      return null;
    }
  }
}

document.addEventListener('click', function(event) {
  anchor = window.myrddinFindParent(event.target, 'a');
  if (anchor != null && 'Show a Map of this Location' == anchor.title) {
    anchor.href.match(/addr=([^&]+)\&csz=([^&]+)/);
    anchor.href = "http://maps.google.com/maps?q=" + RegExp.$1 + ", " + RegExp.$2;
  }
}, true);

This, finally, works. Of course, I should mention I'm leaving out all the very noobish mistakes I made... calls to '.toLower' and '.toLowerCase' rather than the correct '.toLowerCase()'. Calls to '.parent' rather than '.parentNode'. These are the semi-working intermediates... you can fill in the dozens of versions with typos, missed semicolons, and improper names with a little imagination.

This function isn't the best either. I'd rather use an anonymous function for the recursive FindParent, but I don't know how to call an anonymous function recursively, or if it's even possible. So I pollute the global namespace slightly.

The final script, with a final tweak to prevent it from editing the same link twice, I uploaded as my first Greasemonkey script.

Go me.