Handling events in JavaScript: Key events

1. Key Events

When a key on the keyboard is pressed, your browser fires a “keydown” event. When it is released, you get a “keyup” event.

<p>This page turns violet when you hold the V key.</p>

<script>

window.addEventListener(“keydown”, event => {

if (event.key == “v”) {

document.body.style.background = “violet”;

}

});

window.addEventListener(“keyup”, event => {

if (event.key == “v”) {

document.body.style.background = “”;

}

});

</script>

Despite its name, “keydown” fires not only when the key is physically pushed down. When a key is pressed and held, the event fires again every time the key repeats. Sometimes you have to be careful about this. For exam­ple, if you add a button to the DOM when a key is pressed and remove it again when the key is released, you might accidentally add hundreds of but­tons when the key is held down longer.

The example looked at the key property of the event object to see which key the event is about. This property holds a string that, for most keys, cor­responds to the thing that pressing that key would type. For special keys such as enter, it holds a string that names the key (“Enter”, in this case). If you hold shift while pressing a key, that might also influence the name of the key—”v” becomes “V”, and “1” may become “!”, if that is what pressing shift-1 produces on your keyboard.

Modifier keys such as shift, ctrl, alt, and meta (command on Mac) generate key events just like normal keys. But when looking for key combi­nations, you can also find out whether these keys are held down by looking at the shiftKey, ctrlKey, altKey, and metaKey properties of keyboard and mouse events.

<p>Press Control-Space to continue.</p>

<script>

window.addEventListener(“keydown”, event => {

if (event.key == ” ” && event.ctrlKey) {

console.log(“Continuing!”);

}

});

</script>

The DOM node where a key event originates depends on the element that has focus when the key is pressed. Most nodes cannot have focus unless you give them a tabindex attribute, but things like links, buttons, and form fields can. We’ll come back to form fields in Chapter 18. When nothing in particular has focus, document.body acts as the target node of key events.

When the user is typing text, using key events to figure out what is being typed is problematic. some platforms, most notably the virtual keyboard on Android phones, don’t fire key events. But even when you have an old- fashioned keyboard, some types of text input don’t match key presses in a straightforward way, such as input method editor (IME) software used by people whose scripts don’t fit on a keyboard, where multiple key strokes are combined to create characters.

To notice when something was typed, elements that you can type into, such as the <input> and <textarea> tags, fire “input” events whenever the user changes their content. To get the actual content that was typed, it is best to directly read it from the focused field. “Form Fields” on page 317 will show how.

2. Pointer Events

There are currently two widely used ways to point at things on a screen: mice (including devices that act like mice, such as touchpads and trackballs) and touchscreens. These produce different kinds of events.

Mouse Clicks

Pressing a mouse button causes a number of events to fire. The “mousedown” and “mouseup” events are similar to “keydown” and “keyup” and fire when the button is pressed and released. These happen on the DOM nodes that are immediately below the mouse pointer when the event occurs.

After the “mouseup” event, a “click” event fires on the most specific node that contained both the press and the release of the button. For example, if I press down the mouse button on one paragraph and then move the pointer to another paragraph and release the button, the “click” event will happen on the element that contains both those paragraphs.

If two clicks happen close together, a “dblclick” (double-click) event also fires, after the second click event.

To get precise information about the place where a mouse event hap­pened, you can look at its clientX and clientY properties, which contain the event’s coordinates (in pixels) relative to the top-left corner of the window, or pageX and pageY, which are relative to the top-left corner of the whole doc­ument (which may be different when the window has been scrolled).

The following implements a primitive drawing program. Every time you click the document, it adds a dot under your mouse pointer. See Chapter 19 for a less primitive drawing program.

<style>

body {

height: 200px;

background: beige;

}

.dot {

height: 8px;

width: 8px;

border-radius: 4px;

/* rounds corners

*/ background: blue; position: absolute;

}

</style>

<script>

window.addEventListener(“click”, event => {

let dot = document.createElement(“div”);

dot.className = “dot”;

dot.style.left = (event.pageX – 4) + “px”;

dot.style.top = (event.pageY – 4) + “px”;

document.body.appendChild(dot);

});

</script>

Mouse Motion

Every time the mouse pointer moves, a “mousemove” event is fired. This event can be used to track the position of the mouse. A common situation in which this is useful is when implementing some form of mouse-dragging functionality.

As an example, the following program displays a bar and sets up event handlers so that dragging to the left or right on this bar makes it narrower or wider:

<p>Drag the bar to change its width:</p>

<div style=”background: orange; width: 60px; height: 20px”>

</div>

<script>

let lastX; // Tracks the last observed mouse X position let bar = document.querySelector(“div”);

bar.addEventListener(“mousedown”, event => {

if (event.button == 0) {

lastX = event.clientX;

window.addEventListener(“mousemove”, moved);

event.preventDefault();

// Prevent selection

}

});

function moved(event) {

if (event.buttons == 0) {

window.removeEventListener(“mousemove”, moved);

} else {

let dist = event.clientX – lastX;

let newWidth = Math.max(10, bar.offsetWidth + dist);

bar.style.width = newWidth + “px”;

lastX = event.clientX;

}

}

</script>

The resulting page looks like this: Drag the bar to change its width:

Note that the “mousemove” handler is registered on the whole window. Even if the mouse goes outside of the bar during resizing, as long as the but­ton is held we still want to update its size.

We must stop resizing the bar when the mouse button is released. For that, we can use the buttons property (note the plural), which tells us about the buttons that are currently held down. When this is zero, no buttons are down. When buttons are held, its value is the sum of the codes for those buttons—the left button has code 1, the right button 2, and the middle one 4. That way, you can check whether a given button is pressed by tak­ing the remainder of the value of buttons and its code.

Note that the order of these codes is different from the one used by button, where the middle button came before the right one. As men­tioned, consistency isn’t really a strong point of the browser’s program­ming interface.

Touch Events

The style of graphical browser that we use was designed with mouse inter­faces in mind, at a time where touchscreens were rare. To make the web “work” on early touchscreen phones, browsers for those devices pretended, to a certain extent, that touch events were mouse events. If you tap your screen, you’ll get “mousedown”, “mouseup”, and “click” events.

But this illusion isn’t very robust. A touchscreen works differently from a mouse: it doesn’t have multiple buttons, you can’t track the finger when it isn’t on the screen (to simulate “mousemove”), and it allows multiple fingers to be on the screen at the same time.

Mouse events cover touch interaction only in straightforward cases—if you add a “click” handler to a button, touch users will still be able to use it. But something like the resizeable bar in the previous example does not work on a touchscreen.

There are specific event types fired by touch interaction. When a finger starts touching the screen, you get a “touchstart” event. When it is moved while touching, “touchmove” events fire. Finally, when it stops touching the screen, you’ll see a “touchend” event.

Because many touchscreens can detect multiple fingers at the same time, these events don’t have a single set of coordinates associated with them. Rather, their event objects have a touches property, which holds an array-like object of points, each of which has its own clientX, clientY, pageX, and pageY properties.

You could do something like this to show red circles around every touch­ing finger:

<style>

dot {

position: absolute;

display: block;

border: 2px solid red;

border-radius: 50px;

height: 100px;

width: 100px;

}

</style>

<p>Touch this page</p>

<script>

function update(event) {

for (let dot; dot = document.querySelector(“dot”);) {

dot.remove();

}

for (let i = 0; i < event.touches.length; i++) {

let {pageX, pageY} = event.touches[i];

let dot = document.createElement(“dot”);

dot.style.left = (pageX – 50) + “px”;

dot.style.top = (pageY – 50) + “px”;

document.body.appendChild(dot);

}

}

window.addEventListener(“touchstart”, update);

window.addEventListener(“touchmove”, update);

window.addEventListener(“touchend”, update);

</script>

You’ll often want to call preventDefault in touch event handlers to over­ride the browser’s default behavior (which may include scrolling the page on swiping) and to prevent the mouse events from being fired, for which you may also have a handler.

3. Scroll Events

Whenever an element is scrolled, a “scroll” event is fired on it. This has vari­ous uses, such as knowing what the user is currently looking at (for disabling off-screen animations or sending spy reports to your evil headquarters) or showing some indication of progress (by highlighting part of a table of con­tents or showing a page number).

The following example draws a progress bar above the document and updates it to fill up as you scroll down:

<style>

#progress {

border-bottom: 2px solid blue;

width: 0;

position: fixed;

top: 0;

left: 0;

}

</style>

<div id=”progress”></div>

<script>

// Create some content

document.body.appendChild(document.createTextNode( “supercalifragilisticexpialidocious “.repeat(1000)));

let bar = document.querySelector(“#progress”);

window.addEventListener(“scroll”, () => {

let max = document.body.scrollHeight – innerHeight;

bar.style.width = ‘${(pageYOffset / max) * 100}%’; });

</script>

Giving an element a position of fixed acts much like an absolute position but also prevents it from scrolling along with the rest of the document. The effect is to make our progress bar stay at the top. Its width is changed to indi­cate the current progress. We use %, rather than px, as a unit when setting the width so that the element is sized relative to the page width.

The global innerHeight binding gives us the height of the window, which we have to subtract from the total scrollable height—you can’t keep scrolling when you hit the bottom of the document. There is also an innerWidth for the window width. By dividing pageYOffset, the current scroll position, by the maximum scroll position and multiplying by 100, we get the percentage for the progress bar.

Calling preventDefault on a scroll event does not prevent the scrolling from happening. In fact, the event handler is called only after the scrolling takes place.

4. Focus Events

When an element gains focus, the browser fires a “focus” event on it. When it loses focus, the element gets a “blur” event.

Unlike the events discussed earlier, these two events do not propagate.

A handler on a parent element is not notified when a child element gains or loses focus.

The following example displays help text for the text field that currently has focus:

<p>Name: <input type=”text” data-help=”Your full name”></p> <p>Age: <input type=”text” data-help=”Age in years”></p>

<p id=”help”></p>

<script>

let help = document.querySelector(“#help”);

let fields = document.querySelectorAll(“input”);

for (let field of Array.from(fields)) {

field.addEventListener(“focus”, event => {

let text = event.target.getAttribute(“data-help”);

help.textContent = text;

});

field.addEventListener(“blur”, event => { help.textContent = “”;

});

}

</script>

This screenshot shows the help text for the age field.

The window object will receive “focus” and “blur” events when the user moves from or to the browser tab or window in which the document is shown.

5. Load Event

When a page finishes loading, the “load” event fires on the window and the document body objects. This is often used to schedule initialization actions that require the whole document to have been built. Remember that the content of <script> tags is run immediately when the tag is encountered.

This may be too soon, for example when the script needs to do something with parts of the document that appear after the <script> tag.

Elements such as images and script tags that load an external file also have a “load” event that indicates the files they reference were loaded. Like the focus-related events, loading events do not propagate.

When a page is closed or navigated away from (for example, by fol­lowing a link), a “beforeunload” event fires. The main use of this event is to prevent the user from accidentally losing work by closing a document. Pre­venting the page from unloading is not, as you might expect, done with the preventDefault method. Instead, it is done by returning a non-null value from the handler. When you do that, the browser will show the user a dialog ask­ing if they are sure they want to leave the page. This mechanism ensures that a user is always able to leave, even on malicious pages that would prefer to keep them there forever and force them to look at dodgy weight-loss ads.

Source: Haverbeke Marijn (2018), Eloquent JavaScript: A Modern Introduction to Programming, No Starch Press; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *