To add interesting functionality to a web-page, just being able to inspect or modify the document is generally not enough. We also need to be able to detect what the user is doing, and respond to it. For this, we will use a thing called event handlers. Pressed keys are events, mouse clicks are events, even mouse motion can be seen as a series of events. In Web programming: A crash course, we added an onclick property to a button, in order to do something when it was pressed. This is a simple event handler.
The way browser events work is, fundamentally, very simple. It is possible to register handlers for specific event types and specific DOM nodes. Whenever an event occurs, the handler for that event, if any, is called. For some events, such as key presses, knowing just that the event occurred is not good enough, you also want to know which key was pressed. To store such information, every event creates an event object, which the handler can look at.
It is important to realise that, even though events can fire at any time, no two handlers ever run at the same moment. If other JavaScript code is still running, the browser waits until it finishes before it calls the next handler. This also holds for code that is triggered in other ways, such as with setTimeout. In programmer jargon, browser JavaScript is single-threaded, there are never two ‘threads’ running at the same time. This is, in most cases, a good thing. It is very easy to get strange results when multiple things happen at the same time.
An event, when not handled, can ‘bubble’ through the DOM tree. What this means is that if you click on, for example, a link in a paragraph, any handlers associated with the link are called first. If there are no such handlers, or these handlers do not indicate that they have finished handling the event, the handlers for the paragraph, which is the parent of the link, are tried. After that, the handlers for document.body get a turn. Finally, if no JavaScript handlers have taken care of the event, the browser handles it. When clicking a link, this means that the link will be followed.
So, as you see, events are easy. The only hard thing about them is that browsers, while all supporting more or less the same functionality, support this functionality through different interfaces. As usual, the most incompatible browser is Internet Explorer, which ignores the standard that most other browsers follow. After that, there is Opera, which does not properly support some useful events, such as the onunload event which fires when leaving a page, and sometimes gives confusing information about keyboard events.
There are four event-related actions one might want to take.
None of them work the same across all major browsers.
As a practice field for our event-handling, we open a document with a button and a text field. Keep this window open (and attached) for the rest of the chapter.
attach(window.open("example_events.html"));
The first action, registering a handler, can be done by setting an element’s onclick (or onkeypress, and so on) property. This does in fact work across browsers, but it has an important drawback ― you can only attach one handler to an element. Most of the time, one is enough, but there are cases, especially when a program has to be able to work together with other programs (which might also be adding handlers), that this is annoying.
In Internet Explorer, one can add a click handler to a button like this:
$("button").attachEvent("onclick", function(){print("Click!");});
On the other browsers, it goes like this:
$("button").addEventListener("click", function(){print("Click!");},
false);
Note how "on" is left off in the second case. The third argument to addEventListener, false, indicates that the event should “bubble” through the DOM tree as normal. Giving true instead can be used to give this handler priority over the handlers ‘beneath’ it, but since Internet Explorer does not support such a thing, this is rarely useful.
Removing events works very much like adding them, but this time the methods detachEvent and removeEventListener are used. Note that, to remove a handler, you need to have access to the function you attached to it.
function unregisterEventHandler(node, event, handler) {
if (typeof node.removeEventListener == "function")
node.removeEventListener(event, handler, false);
else
node.detachEvent("on" + event, handler);
}
Exceptions produced by event handlers can, because of technical limitations, not be caught by the console. Thus, they are handled by the browser, which might mean they get hidden in some kind of “error console” somewhere, or cause a message to pop up. When you write an event handler and it does not seem to work, it might be silently aborting because it causes some kind of error.
Most browsers pass the event object as an argument to the handler. Internet Explorer stores it in the top-level variable called event. When looking at JavaScript code, you will often come across something like event || window.event, which takes the local variable event or, if that is undefined, the top-level variable by that same name.
function showEvent(event) {
show(event || window.event);
}
registerEventHandler($("textfield"), "keypress", showEvent);
Type a few characters in the field, look at the objects, and shut it up again:
unregisterEventHandler($("textfield"), "keypress", showEvent);
When the user clicks his mouse, three events are generated. First mousedown, at the moment the mouse button is pressed. Then, mouseup, at the moment it is released. And finally, click, to indicate something was clicked. When this happens two times in quick succession, a dblclick (double-click) event is also generated. Note that it is possible for the mousedown and mouseup events to happen some time apart ― when the mouse button is held for a while.
When you attach an event handler to, for example, a button, the fact that it has been clicked is often all you need to know. When the handler, on the other hand, is attached to a node that has children, clicks from the children will ‘bubble’ up to it, and you will want to find out which child has been clicked. For this purpose, event objects have a property called target... or srcElement, depending on the browser.
Another interesting piece of information are the precise coordinates at which the click occurred. Event objects related to the mouse contain clientX and clientY properties, which give the x and y coordinates of the mouse, in pixels, on the screen. Documents can scroll, though, so often these coordinates do not tell us much about the part of the document that the mouse is over. Some browsers provide pageX and pageY properties for this purpose, but others (guess which) do not. Fortunately, the information about the amount of pixels the document has been scrolled can be found in document.body.scrollLeft and document.body.scrollTop.
This handler, attached to the whole document, intercepts all mouse clicks, and prints some information about them.
function reportClick(event) {
event = event || window.event;
var target = event.target || event.srcElement;
var pageX = event.pageX, pageY = event.pageY;
if (pageX == undefined) {
pageX = event.clientX + document.body.scrollLeft;
pageY = event.clientY + document.body.scrollTop;
}
alert("Mouse clicked at ", pageX, ", ", pageY, ". Inside element:");
alert(target);
}
registerEventHandler(document, "click", reportClick);
And get rid of it again:
unregisterEventHandler(document, "click", reportClick);
Obviously, writing all these checks and workarounds is not something you want to do in every single event handler. In a moment, after we have gotten acquainted with a few more incompatibilities, we will write a function to “normalise” event objects to work the same across browsers.
It is also sometimes possible to find out which mouse button was pressed, using the which and button properties of event objects. Unfortunately, this is very unreliable ― some browsers pretend mouses have only one button, others report right-clicks as clicks during which the control key was held down, and so on.
Apart from clicks, we might also be interested in the movement of the mouse. The mousemove event of a DOM node is fired whenever the mouse moves while it is over that element. There are also mouseover and mouseout, which are fired only when the mouse enters or leaves a node. For events of this last type, the target (or srcElement) property points at the node that the event is fired for, while the relatedTarget (or toElement, or fromElement) property gives the node that the mouse came from (for mouseover) or left to (for mouseout).
mouseover and mouseout can be tricky when they are registered on an element that has child nodes. Events fired for the child nodes will bubble up to the parent element, so you will also see a mouseover event when the mouse enters one of the child nodes. The target and relatedTarget properties can be used to detect (and ignore) such events.
For every key that the user presses, three events are generated: keydown, keyup, and keypress. In general, you should use the first two in cases where you really want to know which key was pressed, for example when you want to do something when the arrow keys are pressed. keypress, on the other hand, is to be used when you are interested in the character that is being typed. The reason for this is that there is often no character information in keyup and keydown events, and Internet Explorer does not generate a keypress event at all for special keys such as the arrow keys.
Finding out which key was pressed can be quite a challenge by itself. For keydown and keyup events, the event object will have a keyCode property, which contains a number. Most of the time, these codes can be used to identify keys in a reasonably browser-independant way. Finding out which code corresponds to which key can be done by simple experiments...
function printKeyCode(event) {
event = event || window.event;
alert("Key ", event.keyCode, " was pressed.");
}
registerEventHandler($("textfield"), "keydown", printKeyCode);
unregisterEventHandler($("textfield"), "keydown", printKeyCode);
In most browsers, a single key code corresponds to a single physical key on your keyboard. The Opera browser, however, will generate different key codes for some keys depending on whether shift is pressed or not. Even worse, some of these shift-is-pressed codes are the same codes that are also used for other keys ― shift-9, which on most keyboards is used to type a parenthesis, gets the same code as the down arrow, and as such is hard to distinguish from it. When this threatens to sabotage your programs, you can usually resolve it by ignoring key events that have shift pressed.
To find out whether the shift, control, or alt key was held during a key or mouse event, you can look at the shiftKey, ctrlKey, and altKey properties of the event object.
For keypress events, you will want to know which character was typed. The event object will have a charCode property, which, if you are lucky, contains the Unicode number corresponding to the character that was typed, which can be converted to a 1-character string by using String.fromCharCode. Unfortunately, some browsers do not define this property, or define it as 0, and store the character code in the keyCode property instead.
function printCharacter(event) {
event = event || window.event;
var charCode = event.charCode;
if (charCode == undefined || charCode === 0)
charCode = event.keyCode;
alert("Character '", String.fromCharCode(charCode), "'");
}
registerEventHandler($("textfield"), "keypress", printCharacter);
unregisterEventHandler($("textfield"), "keypress", printCharacter);
An event handler can ‘stop’ the event it is handling. There are two different ways to do this. You can prevent the event from bubbling up to parent nodes and the handlers defined on those, and you can prevent the browser from taking the standard action associated with such an event. It should be noted that browsers do not always follow this ― preventing the default behaviour for the pressing of certain “hotkeys” will, on many browsers, not actually keep the browser from executing the normal effect of these keys.
On most browsers, stopping event bubbling is done with the stopPropagation method of the event object, and preventing default behaviour is done with the preventDefault method. For Internet Explorer, this is done by setting the cancelBubble property of this object to true, and the returnValue property to false, respectively.
And that was the last of the long list of incompatibilities that we will discuss in this chapter. Which means that we can finally write the event normaliser function and move on to more interesting things.
function normaliseEvent(event) {
if (!event.stopPropagation) {
event.stopPropagation = function() {this.cancelBubble = true;};
event.preventDefault = function() {this.returnValue = false;};
}
if (!event.stop) {
event.stop = function() {
this.stopPropagation();
this.preventDefault();
};
}
if (event.srcElement && !event.target)
event.target = event.srcElement;
if ((event.toElement || event.fromElement) && !event.relatedTarget)
event.relatedTarget = event.toElement || event.fromElement;
if (event.clientX != undefined && event.pageX == undefined) {
event.pageX = event.clientX + document.body.scrollLeft;
event.pageY = event.clientY + document.body.scrollTop;
}
if (event.type == "keypress") {
if (event.charCode === 0 || event.charCode == undefined)
event.character = String.fromCharCode(event.keyCode);
else
event.character = String.fromCharCode(event.charCode);
}
return event;
}
A stop method is added, which cancels both the bubbling and the default action of the event. Some browsers already provide this, in which case we leave it as it is.
Next we can write convenient wrappers for registerEventHandler and unregisterEventHandler:
function addHandler(node, type, handler) {
function wrapHandler(event) {
handler(normaliseEvent(event || window.event));
}
registerEventHandler(node, type, wrapHandler);
return {node: node, type: type, handler: wrapHandler};
}
function removeHandler(object) {
unregisterEventHandler(object.node, object.type, object.handler);
}
var blockQ = addHandler($("textfield"), "keypress", function(event) {
if (event.character.toLowerCase() == "q")
event.stop();
});
The new addHandler function wraps the handler function it is given in a new function, so it can take care of normalising the event objects. It returns an object that can be given to removeHandler when we want to remove this specific handler. Try typing a ‘q‘ in the text field.
removeHandler(blockQ);
Armed with addHandler and the dom function from the last chapter, we are ready for more challenging feats of document-manipulation. As an exercise, we will implement the game known as Sokoban. This is something of a classic, but you may not have seen it before. The rules are this: There is a grid, made up of walls, empty space, and one or more “exits”. On this grid, there are a number of crates or stones, and a little dude that the player controls. This dude can be moved horizontally and vertically into empty squares, and can push the boulders around, provided that there is empty space behind them. The goal of the game is to move a given number of boulders into the exits.
Just like the terraria from Object-oriented Programming, a Sokoban level can be represented as text. The variable sokobanLevels, in the example_events.html window, contains an array of level objects. Each level has a property field, containing a textual representation of the level, and a property boulders, indicating the amount of boulders that must be expelled to finish the level.
alert(sokobanLevels.length);
alert(sokobanLevels[1].boulders);
forEach(sokobanLevels[1].field, print);
In such a level, the # characters are walls, spaces are empty squares, 0 characters are used for for boulders, an @ for the starting location of the player, and a * for the exit.
But, when playing the game, we do not want to be looking at this textual representation. Instead, we will put a table into the document. I made small style-sheet (sokoban.css, if you are curious what it looks like) to give the cells of this table a fixed square size, and added it to the example document. Each of the cells in this table will get a background image, representing the type of the square (empty, wall, or exit). To show the location of the player and the boulders, images are added to these table cells, and moved to different cells as appropriate.
It would be possible to use this table as the main representation of our data ― when we want to look whether there is a wall in a given square, we just inspect the background of the appropriate table cell, and to find the player, we just search for the image node with the correct src property. In some cases, this approach is practical, but for this program I chose to keep a separate data structure for the grid, because it makes things much more straightforward.
This data structure is a two-dimensional grid of objects, representing the squares of the playing field. Each of the objects must store the type of background it has and whether there is a boulder or player present in that cell. It should also contain a reference to the table cell that is used to display it in the document, to make it easy to move images in and out of this table cell.
That gives us two kinds of objects ― one to hold the grid of the playing field, and one to represent the individual cells in this grid. If we want the game to also do things like moving the next level at the appropriate moment, and being able to reset the current level when you mess up, we will also need a “controller” object, which creates or removes the field objects at the appropriate moment. For convenience, we will be using the prototype approach outlined at the end of Object-oriented Programming, so object types are just prototypes, and the create method, rather than the new operator, is used to make new objects.
Let us start with the objects representing the squares of the game’s field. They are responsible for setting the background of their cells correctly, and adding images as appropriate. The img/sokoban/ directory contains a set of images, based on another ancient game, which will be used to visualise the game. For a start, the Square prototype could look like this.
var Square = {
construct: function(character, tableCell) {
this.background = "empty";
if (character == "#")
this.background = "wall";
else if (character == "*")
this.background = "exit";
this.tableCell = tableCell;
this.tableCell.className = this.background;
this.content = null;
if (character == "0")
this.content = "boulder";
else if (character == "@")
this.content = "player";
if (this.content != null) {
var image = dom("IMG", {src: "img/sokoban/" +
this.content + ".gif"});
this.tableCell.appendChild(image);
}
},
hasPlayer: function() {
return this.content == "player";
},
hasBoulder: function() {
return this.content == "boulder";
},
isEmpty: function() {
return this.content == null && this.background == "empty";
},
isExit: function() {
return this.background == "exit";
}
};
var testSquare = Square.create("@", dom("TD"));
alert(testSquare.hasPlayer());
The character argument to the constructor will be used to transform characters from the level blueprints into actual Square objects. To set the background of the cells, style-sheet classes are used (defined in sokoban.css), which are assigned to the td elements’ className property.
The methods like hasPlayer and isEmpty are a way to ‘isolate’ the code that uses objects of this type from the internals of the objects. They are not strictly necessary in this case, but they will make the other code look better.
The next object type will be called SokobanField. Its constructor is given an object from the sokobanLevels array, and is responsible for building both a table of DOM nodes and a grid of Square objects. This object will also take care of the details of moving the player and boulders around, through a move method that is given an argument indicating which way the player wants to move.
To identify the individual squares, and to indicate directions, we will again use the Point object type from Object-oriented Programming, which, as you might remember, has an add method.
The base of the field prototype looks like this:
var SokobanField = {
construct: function(level) {
var tbody = dom("TBODY");
this.squares = [];
this.bouldersToGo = level.boulders;
for (var y = 0; y < level.field.length; y++) {
var line = level.field[y];
var tableRow = dom("TR");
var squareRow = [];
for (var x = 0; x < line.length; x++) {
var tableCell = dom("TD");
tableRow.appendChild(tableCell);
var square = Square.create(line.charAt(x), tableCell);
squareRow.push(square);
if (square.hasPlayer())
this.playerPos = new Point(x, y);
}
tbody.appendChild(tableRow);
this.squares.push(squareRow);
}
this.table = dom("TABLE", {"class": "sokoban"}, tbody);
this.score = dom("DIV", null, "...");
this.updateScore();
},
getSquare: function(position) {
return this.squares[position.y][position.x];
},
updateScore: function() {
this.score.firstChild.nodeValue = this.bouldersToGo +
" boulders to go.";
},
won: function() {
return this.bouldersToGo <= 0;
}
};
var testField = SokobanField.create(sokobanLevels[0]);
alert(testField.getSquare(new Point(10, 2)).content);
The constructor goes over the lines and characters in the level, and stores the Square objects in the squares property. When it encounters the square with the player, it saves this position as playerPos, so that we can easily find the square with the player later on. getSquare is used to find a Square object corresponding to a certain x,y position on the field. Note that it doesn’t take the edges of the field into account ― to avoid writing some boring code, we assume that the field is properly walled off, making it impossible to walk out of it.
The word "class" in the dom call that makes the table node is quoted as a string. This is necessary because class is a ‘reserved word’ in JavaScript, and may not be used as a variable or property name.
The amount of boulders that have to be cleared to win the level (this may be less than the total amount of boulders on the level) is stored in bouldersToGo. Whenever a boulder is brought to the exit, we can subtract 1 from this, and see whether the game is won yet. To show the player how he is doing, we will have to show this amount somehow. For this purpose, a div element with text is used. div nodes are containers without inherent markup. The score text can be updated with the updateScore method. The won method will be used by the controller object to determine when the game is over, so the player can move on to the next level.
If we want to actually see the playing field and the score, we will have to insert them into the document somehow. That is what the place method is for. We’ll also add a remove method to make it easy to remove a field when we are done with it.
SokobanField.place = function(where) {
where.appendChild(this.score);
where.appendChild(this.table);
};
SokobanField.remove = function() {
removeElement(this.score);
removeElement(this.table);
};
testField.place(document.body);
If all went well, you should see a Sokoban field now.
All the “game logic” has been taken care of now, and we just need a controller to make it playable. The controller will be an object type called SokobanGame, which is responsible for the following things:
We start again with an unfinished prototype.
var SokobanGame = {
construct: function(place) {
this.level = null;
this.field = null;
var newGame = dom("BUTTON", null, "New game");
addHandler(newGame, "click", method(this, "newGame"));
var reset = dom("BUTTON", null, "Reset level");
addHandler(reset, "click", method(this, "reset"));
this.container = dom("DIV", null,
dom("H1", null, "Sokoban"),
dom("DIV", null, newGame, " ", reset));
place.appendChild(this.container);
addHandler(document, "keydown", method(this, "keyDown"));
this.newGame();
},
newGame: function() {
this.level = 0;
this.reset();
},
reset: function() {
if (this.field)
this.field.remove();
this.field = SokobanField.create(sokobanLevels[this.level]);
this.field.place(this.container);
},
keyDown: function(event) {
// To be filled in
}
};
The constructor builds a div element to hold the field, along with two buttons and a title. Note how method is used to attach methods on the this object to events.
We can put a Sokoban game into our document like this:
var sokoban = SokobanGame.create(document.body);
Other event types that can be useful are focus and blur, which are fired on elements that can be ‘focused’, such as form inputs. focus, obviously, happens when you put the focus on the element, for example by clicking on it. blur is JavaScript-speak for ‘unfocus’, and is fired when the focus leaves the element.
addHandler($("textfield"), "focus", function(event) {
event.target.style.backgroundColor = "yellow";
});
addHandler($("textfield"), "blur", function(event) {
event.target.style.backgroundColor = "";
});
Another event related to form inputs is change. This is fired when the content of the input has changed... except that for some inputs, such as text inputs, it does not fire until the element is unfocused.
addHandler($("textfield"), "change", function(event) {
alert("Content of text field changed to '",
event.target.value, "'.");
});
You can type all you want, the event will only fire when you click outside of the input, press tab, or unfocus it in some other way.
Forms also have a submit event, which is fired when they submit. It can be stopped to prevent the submit from taking place. This gives us a much better way to do the form validation we saw in the previous chapter. You just register a submit handler, which stops the event when the content of the form is not valid. That way, when the user does not have JavaScript enabled, the form will still work, it just won’t have instant validation.
Window objects have a load event that fires when the document is fully loaded, which can be useful if your script needs to do some kind of initialisation that has to wait until the whole document is present. For example, the scripts on the pages for this book go over the current chapter to hide solutions to exercises. You can’t do that when the exercises are not loaded yet. There is also an unload event, firing when the user leaves the document, but this is not properly supported by all browsers.
Most of the time it is best to leave the laying out of a document to the browser, but there are effects that can only be produced by having a piece of JavaScript set the exact sizes of some nodes in a document. When you do this, make sure you also listen for resize events on the window, and re-calculate the sizes of your element every time the window is resized.
Finally, I have to tell you something about event handlers that you would rather not know. The Internet Explorer browser (which means, at the time of writing, the browser used by a majority of web-surfers) has a bug that causes values to not be cleaned up as normal: Even when they are no longer used, they stay in the machine’s memory. This is known as a memory leak, and, once enough memory has been leaked, will seriously slow down a computer.
When does this leaking occur? Due to a deficiency in Internet Explorer’s garbage collector, the system whose purpose it is to reclaim unused values, when you have a DOM node that, through one of its properties or in a more indirect way, refers to a normal JavaScript object, and this object, in turn, refers back to that DOM node, both objects will not be collected. This has something to do with the fact that DOM nodes and other JavaScript objects are collected by different systems ― the system that cleans up DOM nodes will take care to leave any nodes that are still referenced by JavaScript objects, and vice versa for the system that collects normal JavaScript values.
As the above description shows, the problem is not specifically related to event handlers. This code, for example, creates a bit of un-collectable memory:
var jsObject = {link: document.body};
document.body.linkBack = jsObject;
Even after such an Internet Explorer browser goes to a different page, it will still hold on to the document.body shown here. The reason this bug is often associated with event handlers is that it is extremely easy to make such circular links when registering a handler. The DOM node keeps references to its handlers, and the handler, most of the time, has a reference to the DOM node. Even when this reference is not intentionally made, JavaScript’s scoping rules tend to add it implicitly. Consider this function:
function addAlerter(element) {
addHandler(element, "click", function() {
alert("Alert! ALERT!");
});
}
The anonymous function that is created by the addAlerter function can ‘see’ the element variable. It doesn’t use it, but that does not matter ― just because it can see it, it will have a reference to it. By registering this function as an event handler on that same element object, we have created a circle.
There are three ways to deal with this problem. The first approach, a very popular one, is to ignore it. Most scripts will only leak a little bit, so it takes a long time and a lot of pages before the problems become noticeable. And, when the problems are so subtle, who’s going to hold you responsible? Programmers given to this approach will often searingly denounce Microsoft for their shoddy programming, and state that the problem is not their fault, so they shouldn’t be fixing it.
Such reasoning is not entirely without merit, of course. But when half your users are having problems with the web-pages you make, it is hard to deny that there is a practical problem. Which is why people working on ‘serious’ sites usually make an attempt not to leak any memory. Which brings us to the second approach: Painstakingly making sure that no circular references between DOM objects and regular objects are created. This means, for example, rewriting the above handler like this:
function addAlerter(element) {
addHandler(element, "click", function() {
alert("Alert! ALERT!");
});
element = null;
}
Now the element variable no longer points at the DOM node, and the handler will not leak. This approach is viable, but requires the programmer to really pay attention.
The third solution, finally, is to not worry too much about creating leaky structures, but to make sure to clean them up when you are done with them. This means unregistering any event handlers when they are no longer needed, and registering an onunload event to unregister the handlers that are needed until the page is unloaded. It is possible to extend an event-registering system, like our addHandler function, to automatically do this. When taking this approach, you must keep in mind that event handlers are not the only possible source of memory leaks ― adding properties to DOM node objects can cause similar problems.