2006/01/17

Idempotent event queue in JavaScript for use with Behaviour, etc.

[caveat: my original copy now does more, i.e. copies arguments and uses "this" properly. Will paste in later.]

So anyway, there's me using my (modified) copy of the very groovy Behaviour to extract all the JavaScript from my nice clean semantic HTML(or JSP (*cough* bleh)), and wondering how I ever lived with the mish-mash I just rescued.

Generally speaking, Behaviour rulesets are applied on page load, (hence Behaviour.addLoadEvent), whereupon all the selectors in all the rulesets are evaluated and all the events are assigned and added.

However, whenever the page changes (as a result of the Prototype Ajax.Updater, for example), the standard way of ensuring that the rules still apply is to call Behaviour.apply() again, which re-assigns all the events to all the elements etc.

BUT, we have two issues here. Fit the first: suppose one element matches two selectors. If we assign our events in the normal way that works 90% of the time, as follows (to take a simple example):
".button" : function(el) { el.onclick = pressButton(el.id); },
".important" : function(el) { el.onclick = warn("Atchung!"); }, //...

For this element:
<a class="important button">click</a>
Here, two events (one for each rule) would be assigned to the same element, and the first one would overwrite the second one since el.onclick can only ever be one thing.

The standard answer to this is simply to use addEventListener (FF/W3C) or attachEvent(MS). This solves our first problem. A second possible answer is to adapt Simon Willison's addLoadEvent code to enable the addition of multiple functions to arbitrary events, as follows:

/*functionally equivalent to the original*/
function addLoadEvent(func) {
addEvent(window, "onload", func);
}

/*generic version*/
function addEvent(obj, evt, func) {
var oldEvt = obj[evt];
if (typeof oldEvt != 'function') {
obj[evt] = func;
} else {
obj[evt] = function() {
oldEvt();
func();
}
}

HOWEVER, when adding events using addEventListener, attachEvent, or addEvent, subsequent calls to Behaviour.apply() will re-add new events to the same elements, with the result that events added via Behaviour will fire once for each re-application of the given ruleset, i.e. potentially far too many times.

The code that demonstrates this is as follows:

var f = function() { alert("foo"); };

var o = new Object();

addEvent(o, "bang", f);
addEvent(o, "bang", f);
addEvent(o, "bang", f);

o.bang();
With this example, using the above addEvent code (and the addEventListener etc. (although I haven't tested it and I'd look mighty stupid if I'm wrong)), the "foo" alert will appear three times. This is not what we want.

My solution was to write my own event queue for which the "add" function was idempotent. This means that adding the same function three times (as above) has no effect on the second and third time. (More properly, an idempotent operation is one in which one or many executions is functionally identical.)

This requires the ability to compare functions, and luckily the toString() of a Function object returns the source of that function - the ideal way to find identical chunks of code.

So, without further ado:

/*
this is functionally equivalent with the original - for reverse
compatibility
*/
function addLoadEvent(func) {
addEvent(window, "onload", func);
}

/*
usage as above, e.g. addEvent(element, "onclick", function() {} );
generalized and adapted from
http://simon.incutio.com/archive/2004/05/26/addLoadEvent
*/
function addEvent(obj, evt, func) {
var oldFunc = obj[evt];

if (typeof oldFunc != 'function') {
obj[evt] = getEvent();
obj[evt].addListener(func);
} else {
if(oldFunc.__EVT_LIST) {
obj[evt].addListener(func);
} else {
obj[evt] = getEvent();
obj[evt].addListener(oldFunc);
obj[evt].addListener(func);
}
}
}

/*
this could be put within the above function, but that causes a memory
leak in IE
*/
function getEvent() {
var list = [];

/*
this extends the array instance to allow
for string comparison of functions
*/
list.hasFunction = function(val) {
for (var i = 0; i != this.length; i++) {
if(this[i].toString() == val.toString()) return true;
}
return false;
}

/*
this is the actual function that is called when the event is fired.
if any of the listeners return false, then false is returned,
otherwise true
*/
var result = function(event) {
var finalResult = true;
for(var i = 0; i != list.length; i++) {
var evtResult = list[i](event);
if(evtResult == false) finalResult = false;
}
return finalResult;
}

/*
this is the function on the event that adds a listener
*/
result.addListener = function(f) {
if(f == null) return;
if(list.hasFunction(f)) return;
list.push(f);
}

/*
this is a debug function - feel free to remove
usage example: window.onload.list();
*/
result.list = function() {
var log = "";
for(var i = 0; i != list.length; i ++) {
log += "<pre>"+list[i]+"</pre><hr/>";
}
var wnd = window.open("", "Event dump");
wnd.document.write(log);
}

/*
this is a semaphore to ensure that we play nice with other code
*/
result.__EVT_LIST = true;

return result;
}


Enjoy. I think a nice touch is the ability to list the events attached to an element by calling (e.g.) window.onload.list(); but this is generally debug stuff.

I should sleep.

"Referer" header not set on HTTP requests originating from assignment to "window.location" variable on IE6

This one is annoying. Suppose you were to click on the below link:

<a href="http://google.com">Google</a>


In both Firefox and IE6 the "Referer" [sic, TBL we love you!] header is set to the URL of the page on which the clicked link existed, e.g.:

GET / HTTP/1.1
Host: google.com
Referer: http://ianso.blogspot.com


However this code, which is functionally (but not semantically) equivalent:

<span onclick="window.location='http://google.com'">Google</span>


Omits the "referer" header from the request it generate. (Ignore for the moment that the example is deliberately facetious. In RL, the onclick might call a function that might call a confirm that might change the page location.)

Why does this suck? Because you may want to be able to launch an operation sequence from a view page, and then return to that page to view changed state upon completion of that operation. And you might want to do the same operation from multiple view pages. Which means that you have to keep track of where you came from in order to direct the user back to the same place afterwards. This is an ideal use case for the "referer" header.

However, if you decide to direct the user to a new page that (for example) had it's URL constructed in JavaScript, then this becomes annoying. The workaround to this trivial, stupid bug that I only need because I'm using a nasty hack is as follows:

function goTo(url) {
var a = document.createElement(a);
if(!a.click) { //only IE has this (at the moment);
window.location = url;
return;
}
a.setAttribute("href", url);
a.style.display = "none";
$("body").appendChild(a); //prototype shortcut
a.click();
}


(Normal caveats apply, i.e. this is probably me being ill and sleep-deprived and casting about wild accusations concerning specks in the eyes of MS developers while smashing windows with the redwood stuck in mine, but anyway.)

2006/01/05

MUSIC! CODE! MONKEYS!

(I should be asleep.)

Firstly, 50FOOTWAVE have published "Free music" which is a 5-track EP of good hard stuff. Available in MP3, etc. and also FLAC. I'm so used to MP3 that the quality of FLAC is like a breath of fresh air.

Secondly, and on this subject: I was recently re-acquainted with just how beautiful a decent record player with nice can sound when given decent vinyl to groove on, esp. Dire Straits guitar solos and classic jazz. So given how I come across new music (friends give it to me, sometimes on a wholesale basis), an ideal music infrastructure begins to look as follows:
  1. MP3 for general music consumption
  2. CDs for archival & liner art
  3. Vinyl for when the music is just so good.
Other random thoughts:
  • Lisp, spreadsheets, and RagTime (from it's description) all seem to embody a spirit of computational fungibility that shows what computers should be like in future. The reason I can't take the aforementioned 3 programs/languages/environments and produce said Nirvana is because computers suck.
  • Closures in JavaScript are nice. var f = function() {} and all that makes events much nicer to use.
  • I've been forced to confront my instinctive fear of big, sophisticated IDEs from gigantic megacorporations. I'm worried that their code is so smart that my job will become no more challenging than that of the average Visual Basic droid. This would suck.

    • This may simply be post-Microsoft-IDE trauma. I remember VB4... VBA... generated code that should never have seen the light of day... *shudder*
    • Legitimate reasons for ignoring these things still exist, lock-in to their own evolutionary path being the biggest and baddest.
    • This is why, if I have to move intelligence into code, I'd rather it was open-source code. That way, when I've trained Rhesus monkeys equipped with build scripts to construct web applications based on my legion of sequence diagrams, then I could hack on the code to make the computer do even more of the boring stuff computers are good at and which I detest. (repeat after me: a good programmer is a lazy programmer...)

  • Speaking of Work:

    • Time was, people would sleep through Winter 'cos there were no crops to harvest and no light to work by. Humanitys biorythms are adjusted to this pattern.
    • Now, we work 8-hour days all year round, and waking up at 7:00 in the dark is a truly crushing way to start the day (not to mention Brussels public transport.)
    • Therefore, why not work 10-hour days for half the year, and 6-hour days for the other half, when I'd rather be in bed? Eh?

  • I'm currently reading a translation of Les Miserables by Victor Hugo (who is a genius), and it truly is an absolutely incredible work.
Last but not least, I would like to re-emphasise how incredibly stupid it to ramble on in a public space when the brain is already half-asleep. Anyway. I can always edit it later.

2006/01/02

Prototype-style shortcut function for XPath expressions

Background: using Prototype is allegedly like using crack - immediately gratifying and bad for the whole application. The wisdom of using for(in) notwithstanding, I dunno.

Anyway, the nicest things of all in Prototype are the shortcuts: $() and $F(), which make my life much more chilled out (pass the pipe dude,) and so I hereby introduce an equivalent for XPath munging: $X().

This needs Sarissa to paper over the differences between IE and FF for this to work. Needless to say, Safari users can go swivel until Apple gets of their butt and improves their XSLT support. </flamebait>. The commented out 'Logger' is using Lumberjack syntax.
function $X(xPathExpr, ctxNode, doc) {
if(!ctxNode) {
ctxNode = document;
}

if(!doc) {
if(ctxNode instanceof Document) {
doc = ctxNode;
} else {
doc = ctxNode.ownerDocument;
}
}

var result = doc.selectNodes(xPathExpr, ctxNode);

if(result == null) {
//Logger.debug("no match for "+xPathExpr);
return null;
}

if(result.length == 1) return result[0];

return result;
}
Usage is as follows:
  • $X("//p") returns all paras in a document.
  • $("//p[@id=foo]") returns one para with id foo.
  • arg[1] optionally specifies the node context (relative root)
  • arg[2] can specify a different document to work on, for example one retrieved via XMLHttpRequest.

Coffee and laptops:

If you can afford a decent laptop, you should also make sure you have nice thick ceramic coffee cups, and saucers, to go alongside it.

Decent coffee cups are harder to tip over than disposable plastic cups.

Posted because I recently killed severely maimed a laptop in the time-honoured tradition of all over-caffienated coders.