Norbert Tamas
Norbert Tamas

Reputation: 4694

How to get the mouse position without events (without moving the mouse)?

Is it possible to get the mouse position with JavaScript after page loads without any mouse movement event (without moving the mouse)?

Upvotes: 392

Views: 335178

Answers (15)

Alex Peterson
Alex Peterson

Reputation: 491

@Tim Down's answer is not performant if you render 2,000 x 2,000 <a> elements:

OK, I have just thought of a way. Overlay your page with a div that covers the whole document. Inside that, create (say) 2,000 x 2,000 elements (so that the :hover pseudo-class will work in IE 6, see), each 1 pixel in size. Create a CSS :hover rule for those elements that changes a property (let's say font-family). In your load handler, cycle through each of the 4 million elements, checking currentStyle / getComputedStyle() until you find the one with the hover font. Extrapolate back from this element to get the co-ordinates within the document.

N.B. DON'T DO THIS.

But you don't have to render 4 million elements at once, instead use binary search. Just use 4 <a> elements instead:

  • Step 1: Consider the whole screen as the starting search area
  • Step 2: Split the search area into 2 x 2 = 4 rectangle <a> elements
  • Step 3: Using the getComputedStyle() function determine in which rectangle mouse hovers
  • Step 4a: Reduce the search area to that rectangle that the mouse was found in and repeat from step 2.
  • Step 4b: Widen the search area if the mouse was not found in any of the rectangles (i.e. because the mouse moved)

This way you would need to repeat these steps max 11 times, considering your screen is not wider than 2048px.

So you will generate max 11 x 4 = 44 <a> elements.

If you don't need to determine the mouse position exactly to a pixel, but say 10px precision is OK. You would repeat the steps at most 8 times, so you would need to draw max 8 x 4 = 32 <a> elements.

Also generating and then destroying the <a> elements is not performant as DOM insertions are generally slow. Instead, you can just reuse the initial 4 <a> elements and adjust their top, left, width and height values as you loop through steps, so you're not modifying the DOM itself, only the layout.

Taking that one step further, creating 4 <a> could still be considered overkill, because you can reuse the same one <a> element for when testing for getComputedStyle() in each rectangle, but at the cost of a slower algorithm: every time we update CSS properties, we have to relinquish control to the browser for a few milliseconds so it can apply the layout updates.

As a concrete code example, run the following snippet and click anywhere to kick off a "find my cursor" run, with an artificial delay between each iteration to show what's happening (and obviously, do not move the mouse pointer while that's running):

const delay = 300;
let quads = [];

function create(tag, ...classes) {
  const element = document.createElement(tag);
  element.classList.add(...classes);
  return element;
};

async function findPointer() {
  // Split up the screen into four quadrants, each
  // at 50% page width and 50% page height:
  const dim = 50;
  const createQuad = (_, pos) => {
    const a = create(`a`, `find-pointer-quad`);
    const { style } = a;
    style.top = pos < 2 ? 0 : `${dim}%`;
    style.left = pos % 2 === 0 ? 0 : `${dim}%`;
    style.width = style.height = `${dim}%`;
    document.body.appendChild(a);
    return a;
  };
  quads = [1,2,3,4].map(createQuad);
  // Then find the cursor!
  return bisect(dim)
}

function bisect(dim) {
  // Which quadrant is the cursor in?
  let hit;
  quads.some((a) => {
    const style = getComputedStyle(a);
    const result = style.getPropertyValue(`--hit`);
    if (result === `1`) return (hit = { style, a });
  });
  
  // If we did not get a hit, it's pretty much guaranteed that the
  // cursor moved and we need to widen the search scope to locate it.
  // The optimal strategy for how to do this is a subject in itself.
  if (!hit) {
    const [q1] = quads;
    const top = parseFloat(q1.style.top) - dim/2;
    const left = parseFloat(q1.style.left) - dim/2;
    quads.forEach(({ style }, pos) => {
      style.top = pos < 2 ? `${top}%` : `${top + dim}%`;
      style.left = pos % 2 === 0 ? `${left}%` : `${left + dim}%`;
      style.width = `${dim}%`;
      style.height = `${dim}%`;
    });
    return new Promise((resolve) =>
      setTimeout(() => resolve(bisect(2 * dim)), delay)
    );
  }

  // If we DO have a hit, are we done?
  const { style, a } = hit;
  const { top, left, width, height } = a.getBoundingClientRect();
  if (width < 3) {
    // Remember to clean up our helper <a>!
    quads.forEach(a => a.remove());
    return {
      x: left + width / 2 + window.scrollX,
      y: top + height / 2 + window.scrollY,
    };
  }

  // We have a hit, but the resolution's too coarse: keep bisecting.
  const ox = a.style.left;
  const oy = a.style.top;
  const nextStep = dim / 2;
  quads.forEach(({ style }, pos) => {
    style.top = pos < 2 ? oy : `${nextStep + parseFloat(oy)}%`;
    style.left = pos % 2 === 0 ? ox : `${nextStep + parseFloat(ox)}%`;
    style.width = `${nextStep}%`;
    style.height = `${nextStep}%`;
  });

  return new Promise((resolve) =>
    setTimeout(() => resolve(bisect(nextStep)), delay)
  );
}

// Note that this event listener is not involved in finding the cursor,
// it's only here as a stand-in for whatever code OP would have in place
// to get the current cursor coordinate.
document.addEventListener(`click`, async () => {
  // get the cursor coordinate...
  const { x, y } = await findPointer()

  // ... and then visually verify that it's the right coordinate:
  const div = create(`div`, `visual-confirmation`);
  div.style.top = `${y - 5}px`;
  div.style.left = `${x - 5}px`;
  document.body.appendChild(div);
});
html,
body {
  height: 100%;
  width: 100%;

  .find-pointer-quad {
    --hit: 0;
    position: absolute;
    border: 1px solid black;
 
    &:hover {
      --hit: 1;
      background: orange;
    }
  }
  
  .visual-confirmation {
    position: absolute;
    border: 1px solid purple;
    border-radius: 100%;
    width: 10px;
    height: 10px;
  }
}

Note that we'll always need a setTimeout, even if we pick a delay of 0, because JS is single-threaded, so any JS execution will block the DOM from updating. Meaning that so if we want to make sure that the element position/dimensions get updated, and the :hover state gets correctly determined, we need to give control back to the browser between iterations.

Also note that we might "lose the cursor" while we're running this, because anything that takes time risks the cursor getting moved. Should that happen, there won't be a hit in any of the quadrants, and we need to wide the search scope until we've reacquired our target.

Upvotes: 31

JHarding
JHarding

Reputation: 1075

What you can do is create variables for the x and y coordinates of your cursor, update them whenever the mouse moves and call a function on an interval to do what you need with the stored position.

The downside to this of course is that at least one initial movement of the mouse is required to have it work. As long as the cursor updates its position at least once, we are able to find its position regardless of whether it moves again.

var cursor_x = -1;
var cursor_y = -1;
document.onmousemove = function(event)
{
 cursor_x = event.pageX;
 cursor_y = event.pageY;
}
setInterval(check_cursor, 1000);
function check_cursor(){console.log('Cursor at: '+cursor_x+', '+cursor_y);}

The preceding code updates once a second with a message of where your cursor is.

Upvotes: 89

Makan
Makan

Reputation: 699

Yes, It's possible.

If you add "mouseover" event to the document it will fire instantly and you can get the mouse position, of course if mouse pointer was over the document.

   document.addEventListener('mouseover', setInitialMousePos, false);

   function setInitialMousePos( event ) {
       console.log( event.clientX, event.clientY);
       document.removeEventListener('mouseover', setInitialMousePos, false);
   }

Previously it was possible to read mouse position through window.event but it's deprecated now.

Upvotes: 5

Celmaun
Celmaun

Reputation: 24752

Here's my solution. It exports window.currentMouseX and window.currentMouseY properties you can use anywhere. It uses the position of a hovered element (if any) initially and afterwards listens to mouse movements to set the correct values.

(function () {
    window.currentMouseX = 0;
    window.currentMouseY = 0;

    // Guess the initial mouse position approximately if possible:
    var hoveredElement = document.querySelectorAll(':hover');
    hoveredElement = hoveredElement[hoveredElement.length - 1]; // Get the most specific hovered element

    if (hoveredElement != null) {
        var rect = hoveredElement.getBoundingClientRect();
        // Set the values from hovered element's position
        window.currentMouseX = window.scrollX + rect.x;
        window.currentMouseY = window.scrollY + rect.y;
    }

    // Listen for mouse movements to set the correct values
    window.addEventListener('mousemove', function (e) {
        window.currentMouseX = e.pageX;
        window.currentMouseY = e.pageY;
    }, /*useCapture=*/true);
}())

Composr CMS Source: https://github.com/ocproducts/composr/commit/a851c19f925be20bc16bfe016be42924989f262e#diff-b162dc9c35a97618a96748639ff41251R1202

Upvotes: 9

GorvGoyl
GorvGoyl

Reputation: 49150

Not mouse position, but, if you're looking for current cursor postion (for use cases like getting last typed character etc) then, below snippet works fine.
This will give you the cursor index related to text content.

window.getSelection().getRangeAt(0).startOffset

Upvotes: -1

SuperNova
SuperNova

Reputation: 3002

Edit 2020: This does not work any more. It seems so, that the browser vendors patched this out. Because the most browsers rely on chromium, it might be in its core.

Old answer: You can also hook mouseenter (this event is fired after page reload, when the mousecursor is inside the page). Extending Corrupted's code should do the trick:

var x = null;
var y = null;
    
document.addEventListener('mousemove', onMouseUpdate, false);
document.addEventListener('mouseenter', onMouseUpdate, false);
    
function onMouseUpdate(e) {
  x = e.pageX;
  y = e.pageY;
  console.log(x, y);
}

function getMouseX() {
  return x;
}

function getMouseY() {
  return y;
}

You can also set x and y to null on mouseleave-event. So you can check if the user is on your page with it's cursor.

Upvotes: 144

joshua
joshua

Reputation: 684

I think i may have a reasonable solution with out counting divs and pixels..lol

Simply use animation frame or a time interval of a function. you will still need a mouse event one time though just to initiate, but technically you position this where ever you like.

Essentially we are tracking a dummy div at all times with out mouse movement.

// create a div(#mydiv) 1px by 1px set opacity to 0 & position:absolute;

Below is the logic..

var x,y;


$('body').mousemove(function( e ) {

    var x = e.clientX - (window.innerWidth / 2);
    var y = e.clientY - (window.innerHeight / 2);
 }


function looping (){

   /* track my div position 60 x 60 seconds!
      with out the mouse after initiation you can still track the dummy div.x & y
      mouse doesn't need to move.*/

   $('#mydiv').x = x;    // css transform x and y to follow 
   $('#mydiv)'.y = y;

   console.log(#mydiv.x etc)

   requestAnimationFrame( looping , frame speed here);
}  

Upvotes: -1

Patrick Berkeley
Patrick Berkeley

Reputation: 2286

Riffing on @SuperNova's answer, here's an approach using ES6 classes that keeps the context for this correct in your callback:

class Mouse {
  constructor() {
    this.x = 0;
    this.y = 0;
    this.callbacks = {
      mouseenter: [],
      mousemove: [],
    };
  }

  get xPos() {
    return this.x;
  }

  get yPos() {
    return this.y;
  }

  get position() {
    return `${this.x},${this.y}`;
  }

  addListener(type, callback) {
    document.addEventListener(type, this); // Pass `this` as the second arg to keep the context correct
    this.callbacks[type].push(callback);
  }

  // `handleEvent` is part of the browser's `EventListener` API.
  // https://developer.mozilla.org/en-US/docs/Web/API/EventListener/handleEvent
  handleEvent(event) {
    const isMousemove = event.type === 'mousemove';
    const isMouseenter = event.type === 'mouseenter';

    if (isMousemove || isMouseenter) {
      this.x = event.pageX;
      this.y = event.pageY;
    }

    this.callbacks[event.type].forEach((callback) => {
      callback();
    });
  }
}

const mouse = new Mouse();

mouse.addListener('mouseenter', () => console.log('mouseenter', mouse.position));
mouse.addListener('mousemove', () => console.log('mousemove A', mouse.position));
mouse.addListener('mousemove', () => console.log('mousemove B', mouse.position));

Upvotes: 2

StefansArya
StefansArya

Reputation: 2888

The most simple solution but not 100% accurate

$(':hover').last().offset()

Result: {top: 148, left: 62.5}
The result depend on the nearest element size and return undefined when user switched the tab

Upvotes: 5

Lonnie Best
Lonnie Best

Reputation: 11344

You do not have to move the mouse to get the cursor's location. The location is also reported on events other than mousemove. Here's a click-event as an example:

document.body.addEventListener('click',function(e)
{
    console.log("cursor-location: " + e.clientX + ',' + e.clientY);
});

Upvotes: 2

AlexTR
AlexTR

Reputation: 812

You could try something similar to what Tim Down suggested - but instead of having elements for each pixel on the screen, create just 2-4 elements (boxes), and change their location, width, height dynamically to divide the yet possible locations on screen by 2-4 recursively, thus finding the mouse real location quickly.

For example - first elements take right and left half of screen, afterwards the upper and lower half. By now we already know in which quarter of screen the mouse is located, are able to repeat - discover which quarter of this space...

Upvotes: 12

user2958613
user2958613

Reputation: 11

I implemented a horizontal/vertical search, (first make a div full of vertical line links arranged horizontally, then make a div full of horizontal line links arranged vertically, and simply see which one has the hover state) like Tim Down's idea above, and it works pretty fast. Sadly, does not work on Chrome 32 on KDE.

jsfiddle.net/5XzeE/4/

Upvotes: 1

user2892032
user2892032

Reputation:

I envision that maybe you have a parent page with a timer and after a certain amount of time or a task is completed, you forward the user to a new page. Now you want the cursor position, and because they are waiting, they aren't necessarily touching the mouse. So track the mouse on the parent page using standard events and pass the last value to the new page in a get or a post variable.

You can use JHarding's code on your parent page so that the latest position is always available in a global variable:

var cursorX;
var cursorY;
document.onmousemove = function(e){
    cursorX = e.pageX;
    cursorY = e.pageY;
}

This won't help users that navigate to this page by means other than your parent page.

Upvotes: -1

Corrupted
Corrupted

Reputation: 83

var x = 0;
var y = 0;

document.addEventListener('mousemove', onMouseMove, false)

function onMouseMove(e){
    x = e.clientX;
    y = e.clientY;
}

function getMouseX() {
    return x;
}

function getMouseY() {
    return y;
}

Upvotes: 1

Tim Down
Tim Down

Reputation: 324477

Real answer: No, it's not possible.

OK, I have just thought of a way. Overlay your page with a div that covers the whole document. Inside that, create (say) 2,000 x 2,000 <a> elements (so that the :hover pseudo-class will work in IE 6, see), each 1 pixel in size. Create a CSS :hover rule for those <a> elements that changes a property (let's say font-family). In your load handler, cycle through each of the 4 million <a> elements, checking currentStyle / getComputedStyle() until you find the one with the hover font. Extrapolate back from this element to get the co-ordinates within the document.

N.B. DON'T DO THIS.

Upvotes: 413

Related Questions