Cross-Browser Event Handling and Memory Leaks

Post sponsored by smtp ghost

In the "old-style" event registration model, you would typically register events by assigning functions to the onevent property of DOM elements:

elem.onclick = function() {
    alert("You clicked me");
}

The problem with that approach is that you can only assign a single event handler function to any given event. All modern browsers support more advanced event registration mechanisms so you can attach multiple event listeners to any given event, though, as usual, those mechanisms vary across platforms. This recipe can be used to register and unregister event listeners in all modern browsers:

function addEventListener(instance, eventName, listener) {
    var listenerFn = listener;
    if (instance.addEventListener) {
        instance.addEventListener(eventName, listenerFn, false);
    } else if (instance.attachEvent) {
        listenerFn = function() {
            listener(window.event);
        }
        instance.attachEvent("on" + eventName, listenerFn);
    } else {
        throw new Error("Event registration not supported");
    }
    return {
        instance: instance,
        name: eventName,
        listener: listenerFn
    };
}

function removeEventListener(event) {
    var instance = event.instance;
    if (instance.removeEventListener) {
        instance.removeEventListener(event.name, event.listener, false);
    } else if (instance.detachEvent) {
        instance.detachEvent("on" + event.name, event.listener);
    }
}

The usage model for the functions above looks like this:

var elem = document.getElementById("elem");
var listener = addEventListener(elem, "click", function() {
    alert("You clicked me!");
});
removeEventListener(listener);

While those recipes function correctly in all major browsers, Internet Explorer has a lot of memory leak bugs that are exacerbated by event registration. While the details are a bit complex (subtle interactions between function closures and COM), we can augment the recipe above with a global event deregistration function that will remove memory leaks in most applications:

var __eventListeners = [];

function addEventListener(instance, eventName, listener) {
    var listenerFn = listener;
    if (instance.addEventListener) {
        instance.addEventListener(eventName, listenerFn, false);
    } else if (instance.attachEvent) {
        listenerFn = function() {
            listener(window.event);
        }
        instance.attachEvent("on" + eventName, listenerFn);
    } else {
        throw new Error("Event registration not supported");
    }
    var event = {
        instance: instance,
        name: eventName,
        listener: listenerFn
    };
    __eventListeners.push(event);
    return event;
}

function removeEventListener(event) {
    var instance = event.instance;
    if (instance.removeEventListener) {
        instance.removeEventListener(event.name, event.listener, false);
    } else if (instance.detachEvent) {
        instance.detachEvent("on" + event.name, event.listener);
    }
    for (var i = 0; i < __eventListeners.length; i++) {
        if (__eventListeners[i] == event) {
            __eventListeners.splice(i, 1);
            break;
        }
    }
}

function unregisterAllEvents() {
    while (__eventListeners.length > 0) {
        removeEventListener(__eventListeners[0]);
    }
}

The unregisterAllEvents function unregisters all events globally, which kills the references between the DOM objects and the listener functions, which generally prevents event registration memory leaks. To take advantage of the function, call it in the onunload handler for your page:

<body onunload="unregisterAllEvents()">

See a complete example

Canceling and Stopping Browser Events

If your Ajax application is graphically intensive, it probably uses a lot of CSS absolute positioned elements and the CSS z-order attribute extensively. The problem with overlapping elements is that events automatically bubble up from foreground elements to background elements, so if you blindly capture events like click in background elements, you may receive them when you are not expecting them. For example, if you have a div element floating above other elements, events will automatically bubble up to background elements, often leading to your application inadvertently handling the event in both the foreground div and the background div.

If you want to prevent events from bubbling up to background elements, you can capture the event with this recipe:

function stopEvent(e) {
    if (!e) e = window.event;
    if (e.stopPropagation) {
        e.stopPropagation();
    } else {
        e.cancelBubble = true;
    }
}

For example, to prevent the container of a link to receive a click event when the link is clicked, you would use the function above like this:

var link = document.getElementById("link");
link.onclick = stopEvent;

The snippet above will prevent the background of the link from receiving a click event, but how do you prevent the link itself from getting the click event? This recipe completely cancels an event, which, in this case, prevents the link from executing at all:

function cancelEvent(e) {
    if (!e) e = window.event;
    if (e.preventDefault) {
        e.preventDefault();
    } else {
        e.returnValue = false;
    }
}

If you want to both cancel the event for the link (i.e., prevent a click from executing the link) and prevent the event from bubbling to background elements, you can combine the two functions like this:

var link = document.getElementById("link");
link.onclick = function(e) {
    cancelEvent(e);
    stopEvent(e);
}

See the example

JavaScript Debug Log

Even with the most advanced debugging tools, every program requires a little "printf debugging": run your program, print out state as it executes, and visually verify that everything is working properly. Unfortunately, there is no built in console to print debug messages to in JavaScript, forcing many JavaScript programmers to revert to calling alert() in their code. alert is completely infeasible for a function that gets called a lot, as it halts execution until you click an OK button. This recipe creates a simple debug log window that does not halt execution:

function log(message) {
    if (!log.window_ || log.window_.closed) {
        var win = window.open("", null, "width=400,height=200," +
                              "scrollbars=yes,resizable=yes,status=no," +
                              "location=no,menubar=no,toolbar=no");
        if (!win) return;
        var doc = win.document;
        doc.write("<html><head><title>Debug Log</title></head>" +
                  "<body></body></html>");
        doc.close();
        log.window_ = win;
    }
    var logLine = log.window_.document.createElement("div");
    logLine.appendChild(log.window_.document.createTextNode(message));
    log.window_.document.body.appendChild(logLine);
}

To print log messages, you just call log() within your code:

for (var i = 0; i < 10; i++) {
    log("This is log message #" + i);
}

When you push your code to production, you can just replace the definition of log with an empty function so your users will not see your debug messages.

See the example

Disabling the Browser Context Menu

You can disable the browser context menu for an element in your DOM with the following JavaScript recipe:

function disableContextMenu(element) {
    element.oncontextmenu = function() {
        return false;
    }
}

For example, to disable the context menu over a photo on your page, you could use:

disableContextMenu(document.getElementById("photo"));

Browsers like Firefox do not always allow web pages to disable the context menu (this option is a setting in Firefox), so this recipe will not work 100% of the time in all browsers.

See the example

Recipe Archive »