Mouse events

  1. Mouse event types
    1. Simple events
    2. Complex events
    3. Events fire order
  2. Getting the button info: which/button
    1. The W3C approach
    2. The IE approach
    3. Cross-browser approach
  3. mouseover/mouseout and relatedTarget
  4. Mousemove and mouseover frequency
    1. Test stand
  5. Mouseout to a child element
    1. The mouseenter/mouseleave events.
  6. Mouse coordinates: clientX(Y),pageX(Y)
    1. Relative to window
    2. Relative to document
      1. IE<9 workaround
    3. The demo of mouse coordinates
  7. Right click: oncontextmenu
  8. Preventing selection
  9. Drag’n’drop
  10. Summary

This section covers properties and specials of mouse-related events.

Mouse event types

Simple events

There are following simplest mouse events:

mousedown
Triggered by an element when a mouse button is pressed down over it
mouseup
Triggered by an element when a mouse button is released over it
mouseover
Triggered by an element when the mouse comes over it
mouseout
Triggered by an element when the mouse goes out of it
mousemove
Triggered by an element on every mouse move over it.

Complex events

Also browser provides the following more complex events for convenience:

click
Triggered by a mouse click: mousedown and then mouseup over an element
contextmenu
Triggered by a right-button mouse click over an element.
dblclick
Triggered by two clicks within a short time over an element

There is also a mousewheel event, but it’s not used. The scroll event is used to track scrolling instead. It occurs on any scroll, including keyboard scroll.

Events fire order

A single action may cause multiple events. For example, a click actually causes mousedown, mouseup and click in sequence.

Just click on the input below to see the events happening. Try double click.


To make the logging more verbose, if there is more than 1 second between events, a dashed line is drawn.

So, for example, for a single click, multiple events are fired. That’s fairly ok. The order is consistent across all browser excepts IE<9 which skips second click on dblclick.

Mouse event handling example:

<input type="button" value="click me" id="btn">
<input type="button" value="right-click me" id="btn2">


<script> 
document.getElementById('btn').onclick = function() {
  alert('click!')
}

document.getElementById('btn2').oncontextmenu = function() {
  alert('right click!')
}
</script>

How to handle `click` and `dblclick`

When we want to handle both click and dblclick, we can’t handle click immediately, because we don’t know i the user is going to click once more.

So the only way is to wait about 100ms, see if a double click is coming. If the time is out, then that’s a single click.

All browsers except IE<9 generate two click events on double click. IE<9 skips second click, try it on the test-stand above. But this logic is tolerant to the difference.

Getting the button info: which/button

For click-related mouse events it may be important, which button was pressed.

For that purpose, the event object contains two properties: which and button. They store the button in a numeric form with few IE/W3C incompatibilities, which you can see in the table below.

The W3C approach

In W3C there is a button property which works same in all browsers except IE:

  • 0 - left button
  • 1 - middle button
  • 2 - right button

The IE approach

Frankly speaking, the Microsoft approach is more universal. The properietary button property is a 3-bit number, every bit is up if the button is pressed.

So, button & 1 (the 1st bit) is set to 1, if the left button is pressed, button & 2 (the 2nd bit) is 1, if the right button is pressed, and button & 4 (the 3rd bit) - if the middle button is pressed.

As the result, we can check if two buttons are pressed in one time. Unfortunately, this is possible only in IE.

Cross-browser approach

The most convenient way here is to take the standard which property as the basis and use button to emulate it for IE:

function fixWhich(e) {
  if (!e.which && e.button) {
    if (e.button & 1) e.which = 1      // Left
    else if (e.button & 4) e.which = 2 // Middle
    else if (e.button & 2) e.which = 3 // Right
  }
}

mouseover/mouseout and relatedTarget

Events of types mouseover (and mouseout) occur when a mouse comes over(goes out) an element.

For these events, the mouse goes from one element to another, so two elements are actually engaged. Both elements can be retrieved from in event properties.

mouseover
The element under the pointer is event.target(IE: srcElement).
The element the mouse came from is event.relatedTarget(IE: fromElement)
mouseout
The element the mouse came from is event.target(IE: srcElement).
The element under the pointer is event.relatedTarget(IE: event.toElement).

As you can see, the W3C specification joins both fromElement and toElement into the single property relatedTarget which serves as fromElement for mouseover and toElement for mouseout.

// from IE to W3C
if (e.relatedTarget === undefined) {
  e.relatedTarget = e.fromElement || e.toElement
}

event.relatedTarget (and IE analogues) can be null.

For example when the mouse comes from out of the window, the mouseover will have relatedTarget = null.

Check it on the iframe below:

The source code in the playground: tutorial/browser/events/mouseoverout.html.

Mousemove and mouseover frequency

Mousemove triggers on every mouse movement. It’s target is the topmost and most nested element which is covered by mouse.

Mousemove and mouseover/mouseout trigger when browser internal timing allows.

That means if you move the mouse fast, intermediate DOM elements and parents are be skipped.

So you can move over an element without any mousemove/mouseover triggered on it.

You can move from a child through parent without any mouse event on the parent.

Although browser can skip intermediate elements, it guarantees that as far as mouseover was triggered, the mouseout will trigger too.

Test stand

Try that on the test stand below. Move the mouse lightningly fast over the elements. There can be no events, or only the red div will get them, or only the green one.

Also try fast-moving it from the red child. The parent will be ignored.

Text


Mouseout to a child element

The mouse pointer can only be over a single element in one moment of time. The one which is topmost, with maximal z-index and deepest, the most nested.

When mouse goes to a child element, the parent triggers mouseout. So it looks like the mouse has left the parent, but it just moved into a child.

The blue DIV in the example below has the event-printing handler.
See how it looks, move the mouse from blue to red.


When moving from the parent to the child, the events are:

  1. mouseout on the parent
  2. mouseover on the child, which bubbles to the parent and triggers it’s handler.

So, there is actually a pair of events, which may spoil the code behavior if not taken into account.

The mouseenter/mouseleave events.

Usually, we don’t want to care about the mouse moving to child elements.
All we want is to know when the mouse enters the element and when it leaves.

There are mouseenter and mouseleave events to handle this, described in DOM Level 3 specification and supported by IE.

For the rest of the browsers, we need to filter out mouseouts to children. The standard trick is to check the relatedTarget, and do nothing if we are still inside the parent.

In the example below, we use mouseout for all browsers, but filter it through isOutside which ascends through parents of relatedTarget until it either meets the parent (this means we’re inside) or reaches the top node (we’re outside).

<div style="padding:10px;border: 1px solid blue" id="parent">
 <p>Move the mouse in and outside of here. The blue box parent has <i>many</i> other <b>elements</b> inside.</p>
 <p>They do not generate extra `mouseover/mouseout` events.</p>
</div>
event

<script>
function isOutside(evt, parent) {
  var elem = evt.relatedTarget || evt.toElement || evt.fromElement

  while ( elem && elem !== parent) {
    elem = elem.parentNode;
  }

  if ( elem !== parent) {
    return true
  }
}

var parent = document.getElementById('parent')

parent.onmouseover = parent.onmouseout = function(e) {
  e = e || event
  
  if (isOutside(e, this)) {
    parent.nextSibling.nodeValue = new Date() + ' ' + e.type 
  } 
}   
</script>

Create a button, which changes on mouse over and click. Hover/Click on it to see:

The source images are at tutorial/browser/events/rollover-src/index.html.

Also, the source code contains helper functions to add/remove class elements. Remember to use semantic markup.

Open hint 1
Hint 1
Open solution
Solution

The button is a DIV and the state is described in CSS:

.button {
  width: 186px;
  height: 52px;    
  background: url(button_static.png);    
}
  
.button-hover {
  background: url(button_hover.png);    
}
    
.button-click {
  background: url(button_click.png);    
}

When the mouse comes over the button, .button-hover class is added.

When the mousedown is detected, .button-click class is added.

Note that button substate classes are below the .button, to make them more prioritized than .button.
So adding/removing these classes actually overrides the background.

Now, the JavaScript part:

button.onmouseover = function() {
  addClass(this, 'button-hover')
}

button.onmouseout = function() {
  removeClass(this, 'button-hover')
  removeClass(this, 'button-click') // (*)
}

button.onmousedown = function() {
  addClass(this, 'button-click')  
}

button.onmouseup = function() {
  removeClass(this, 'button-click')  
}

The mouseout handler also removes clicked state. When a visitor mousedowns over the button then moves the mouse away, it eventually becomes ‘unclicked’.

See the full solution at tutorial/browser/events/rollover/index.html.

CSS sprites

There is a downside of the solution given above. When a user mouseovers a button, and it’s background is changed, the browser may not have the image.

To actally show the new background, the browser has to download the new image.

That takes time, and the state change is not shown until the new image is loaded.

The much better real-life way is to join all button states in a single image, called CSS sprite:

This image is set as background, and background-position is adjusted to show the current state:

.button {
  width: 186px;
  height: 52px;
  background: url(button.png) no-repeat;
}  
  
.button-hover { background-position: 0 -52px; }
    
.button-click { background-position: 0 -104px; }

This solution has no problems with instant reflection of a state change.

See the full example at tutorial/browser/events/rollover-sprite/index.html.

P.S. Note that there is a pure-CSS solution, with :active and :hover pseudoclasses, but it doesn’t work in IE<8 or IE8 in compatibility mode because of bugs with :active handling.

Mouse coordinates: clientX(Y),pageX(Y)

For mouse-related event handling, a cross-browser way of getting coordinates is often needed.

Relative to window

There is a great cross-browser property pair clientX/clientY which contain coordinates relative to window.

If your window is 500x500, and the mouse is in the center, then clientX and clientY are both equal to 250.

If you scroll down, left or up without moving the mouse - the values of clientX/clientY don’t change, because they are relative to the window, not the document.

Move the mouse over the input to see clientX/clientY:

<input onmousemove="this.value = event.clientX+':'+event.clientY">

Relative to document

Usually, to process an event we need mouse position relative to document, with scroll. The W3C standard provides a property pair pageX/pageY for that.

If your window is 500x500, and the mouse is in the middle, then both pageX and pageY equal 250. If you scroll it 250 pixels down, the value of pageY becomes 500.

So, the pair pageX/pageY contains coordinates relative to document top-left corner, with all scrolls.

They are supported by all browsers except IE<9.

Move the mouse over the input to see pageX/pageY (except IE<9):

<input onmousemove="this.value = event.pageX+':'+event.pageY">

IE<9 workaround

In older IEs, page coordinates can be calculated by adding document scroll to clientX/clientY.

If the document is in standards mode, then the page scroll is on HTML element: document.documentElement.scrollLeft, in quirks mode it’s on the BODY: document.body.scrollLeft.

So let’s try both. And if nothing is set (possible if in quirks mode and the body hasn’t loaded yet), then the scroll is 0.

var html = document.documentElement
var body = document.body
e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0)

We’re almost done. But there is one more subtle feature for IE. The document in IE may be shifted from (0,0) position. The shift is kept in document.documentElement.clientLeft/clientTop (no quirks mode), so we’ll need to take it into account as well.

The following code provides a reliable pageX/pageY for IE, even if it’s not there:

function fixPageXY(e) {
  if (e.pageX == null && e.clientX != null ) { 
    var html = document.documentElement
    var body = document.body

    e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0)
    e.pageX -= html.clientLeft || 0
    
    e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0)
    e.pageY -= html.clientTop || 0
  }
}

The demo of mouse coordinates

The following example shows mouse coordinates relative to the document for all browsers.

document.onmousemove = function(e) {
  e = e || window.event
  fixPageXY(e)

  document.getElementById('mouseX').value = e.pageX
  document.getElementById('mouseY').value = e.pageY
}

Coordinate X:
Coordinate Y:

Right click: oncontextmenu

By default, the browser shows it’s own context menu on right mouse click.
But if a JavaScript handler is set, it can suppress the native menu.

The only exception is older versions of Opera which require a special menu option to be enabled. Newer Opera 10.50+ is fine.

<input type="button" oncontextmenu="alert('Custom menu');return false" value="Right-click me"/>

In older version of Opera a typical solution was to replace contextmenu handler Ctrl+click combination or with a long click.

Preventing selection

A common problem with clicks on the text is selection. For example, you want to handle double click. Try double clicking the span below.

<span ondblclick="alert('ok')">Text</span>

The event handler works. But as a side effect, the text becomes selected.

To stop the selection, we should prevent default browser action for selectstart event in IE and mousedown in all other browsers.

The example below triggers click events correctly, but does not become selected.

<span 
  ondblclick="alert('ok')"
  onselectstart="return false"
  onmousedown="return false"
>Text</span>

The method described allow does not make an element unselectable. A user might want to select the text contents, and he is able to do it, for example by starting the mousedown near the element.

Drag’n’drop

An elementary drag’n’drop is easy. The algorithm is:

  1. Track mousedown on the element. When triggers, start the drag’n’drop, assign handlers.
  2. Drag by tracking mousemove. Make the element absolute positioned and move it’s left/top with the mouse.
    By assigning them to event.pageX/pageY you match the top-left corner with the pointer. To put the element under the pointer, a shift is needed.
  3. Finish with mouseup on the element

In the following example, the ball image can be dragged around:

Click the ball and drag to move it.

document.getElementById('ball').onmousedown = function() {
  this.style.position = 'absolute'

  var self = this

  document.onmousemove = function(e) {
    e = e || event
    fixPageXY(e)  
    // put ball center under mouse pointer. 25 is half of width/height
    self.style.left = e.pageX-25+'px' 
    self.style.top = e.pageY-25+'px' 
  }
  this.onmouseup = function() {
    document.onmousemove = null
  }
}

//document.getElementById('ball').ondragstart = function() { return false }

Try it in Firefox, in IE. Doesn’t work well, right?

That’s because of the commented last line. Browser starts it’s own drag’n’drop for the image which spoils our custom processing.

Cancelling browser default action for drag’n’drop is required here.

The fixed example:

Click the ball and drag to move it.

document.getElementById('ball2').onmousedown = function() {
  this.style.position = 'absolute'

  var self = this

  document.onmousemove = function(e) {
    e = e || event
    fixPageXY(e) 
   
    self.style.left = e.pageX-25+'px' 
    self.style.top = e.pageY-25+'px' 
  }
  this.onmouseup = function() {
    document.onmousemove = null
  }
}

document.getElementById('ball2').ondragstart = function() { return false }

In real applications there are additional mouse handlers which detect when the dragged object comes over possible targets and highlight these targets. Also the drop usually leads to more complex processing.

Why `mousemove` is on `document`, not on `ball`?

Really, why? From the first sight, the mouse is always over the ball. The coordinates are always same, no matter which element catches the event. So using ball.onmousemove instead of document.onmousemove may seem fine.

But actually, the mouse is not over the ball. Remember, the browser registers mousemove often, but not for every pixel.

A swift move will trigger mousemove on the far end of the page. That’s why we need to track mousemove on the whole document.

Native drag'n'drop

Several browsers (Firefox, Safari/Chrome) support native drag’n’drop events.

Their main benefit is that they allow to drag an arbitrary file into the browser, so that JavaScript is able to get their binary contents.

For regular in-browser drag’n’drop, mouse events work good enough.

Summary

Mouse events got the following additional standard properties:

  • The mouse button: which
  • Trigger elements: target/relatedTarget
  • Coordinates relative to the window: clientX/clientY
  • Coordinates relative to the document: pageX/pageY

There are incompatibilities around, but they are easily solvable as described above.

Mouseover, mousemove, mouseout have special features:

  1. Mouseover and mouseout are only events which have second target: relatedTarget (toElement || srcElement in IE).
  2. Mouseout triggers when mouse leaves the parent for it’s child. Use mouseenter/mouseleave and their emulation to skip such events.
  3. Mouseover, mousemove, mouseout can skip elements. The mouse may appear immediately over a child skipping all it’s parents.

Additional recipes:

  • You can prevent selection with selectstart/mousedown handlers when making controls from text elements.
  • When using mousedown -> mousemove interaction, like custom drag’n’drop, it is usually required to prevent dragstart.

Create a tree which hides/shows children on click.

The source code of the tree is here.

Open hint 1
Hint 1
Open solution
Solution

The generic approach to the task is to use event delegation: assign a click handler to the tree top and see on which tree node it has happened.

Then get internal UL of this node and show/hide it.

Fix HTML/CSS

The first problem is catching tree node clicks. How do we catch a click on “Mammals” ? The list is a block element, so the click will trigger even aside from the text:

Run the example below and click on the same line with Mammals, but not on Mammals itself. See, it works Sad Smile

<style>
li { border: 1px solid green; }
</style>

<ul onclick="alert(event.target || event.srcElement)">
<li>Mammals
  <ul>
    <li>Cows</li>
    <li>Donkeys</li>
    <li>Dogs</li>
    <li>Tigers</li>
  </ul>
</li>
</ul>

LI is a block element, so it expands for full width. You can see that above, because all LIs have a border.

A visitor can click on an empty space on the right, and that still is LI.

To fix this, it is sufficient to wrap all node titles into SPAN and handle clicks on spans only.

We could do it by changing HTML, but let’s use JavaScript here for additional experience.

The code below finds all LI and wraps text node into span.

var treeUl = document.getElementsByTagName('ul')[0]

var treeLis = treeUl.getElementsByTagName('li')

for(var i=0; i<treeLis.length; i++) {
  var li = treeLis[i]
  
  var span = document.createElement('span')
  li.insertBefore(span, li.firstChild) // add empty span before title
  span.appendChild(span.nextSibling) // move the title into span
}

Now the tree handles title clicks correctly:

<style>
span { border: 1px solid red; }
</style>

<ul onclick="alert((event.target||event.srcElement).nodeName)">
<li><span>Mammals</span>
  <ul>
    <li><span>Cows</span></li>
    <li><span>Donkeys</span></li>
    <li><span>Dogs</span></li>
    <li><span>Tigers</span></li>
  </ul>
</li>
</ul>

The SPAN is an inline element, so it is always same size as the text. Viva la SPAN!

Handle clicks

Implement event delegation. We don’t need to ascent the parent chain, just check if the SPAN was clicked and show/hide it’s next sibling.

var tree = document.getElementsByTagName('ul')[0]

tree.onclick = function(e) {
  e = e || event
  var target = e.target || e.srcElement
    
  if (target.nodeName != 'SPAN') {
    // click on somewhere away from the title
    return;
  }
    
  // get the corresponding UL 
  var node = target.parentNode.getElementsByTagName('ul')[0]
  if (!node) return // no children

  // toggle it
  node.style.display = node.style.display ? '' : 'none'
}

Polishing

The polishing involves:

  • Making spans bold when hovered (CSS)
  • Disabling selection with mousedown/selectstart

You can see the full code of the solution at here.

Tutorial

Donate

Donate to this project