[javascript] How to distinguish mouse "click" and "drag"

I use jQuery.click to handle the mouse click event on Raphael graph, meanwhile, I need to handle mouse drag event, mouse drag consists of mousedown, mouseupand mousemove in Raphael.

It is difficult to distinguish click and drag because click also contain mousedown & mouseup, How can I distinguish mouse "click" & mouse "drag" then in Javascript?

This question is related to javascript dom-events

The answer is


I think the difference is that there is a mousemove between mousedown and mouseup in a drag, but not in a click.

You can do something like this:

const element = document.createElement('div')
element.innerHTML = 'test'
document.body.appendChild(element)
let moved
let downListener = () => {
    moved = false
}
element.addEventListener('mousedown', downListener)
let moveListener = () => {
    moved = true
}
element.addEventListener('mousemove', moveListener)
let upListener = () => {
    if (moved) {
        console.log('moved')
    } else {
        console.log('not moved')
    }
}
element.addEventListener('mouseup', upListener)

// release memory
element.removeEventListener('mousedown', downListener)
element.removeEventListener('mousemove', moveListener)
element.removeEventListener('mouseup', upListener)

This should work well. Similar to the accepted answer (though using jQuery), but the isDragging flag is only reset if the new mouse position differs from that on mousedown event. Unlike the accepted answer, that works on recent versions of Chrome, where mousemove is fired regardless of whether mouse was moved or not.

var isDragging = false;
var startingPos = [];
$(".selector")
    .mousedown(function (evt) {
        isDragging = false;
        startingPos = [evt.pageX, evt.pageY];
    })
    .mousemove(function (evt) {
        if (!(evt.pageX === startingPos[0] && evt.pageY === startingPos[1])) {
            isDragging = true;
        }
    })
    .mouseup(function () {
        if (isDragging) {
            console.log("Drag");
        } else {
            console.log("Click");
        }
        isDragging = false;
        startingPos = [];
    });

You may also adjust the coordinate check in mousemove if you want to add a little bit of tolerance (i.e. treat tiny movements as clicks, not drags).


Based on this answer, I did this in my React component:

export default React.memo(() => {
    const containerRef = React.useRef(null);

    React.useEffect(() => {
        document.addEventListener('mousedown', handleMouseMove);

        return () => document.removeEventListener('mousedown', handleMouseMove);
    }, []);

    const handleMouseMove = React.useCallback(() => {
        const drag = (e) => {
            console.log('mouse is moving');
        };

        const lift = (e) => {
            console.log('mouse move ended');
            window.removeEventListener('mousemove', drag);
            window.removeEventListener('mouseup', this);
        };

        window.addEventListener('mousemove', drag);
        window.addEventListener('mouseup', lift);
    }, []);

    return (
        <div style={{ width: '100vw', height: '100vh' }} ref={containerRef} />
    );
})

You could do this:

_x000D_
_x000D_
var div = document.getElementById("div");
div.addEventListener("mousedown", function() {
  window.addEventListener("mousemove", drag);
  window.addEventListener("mouseup", lift);
  var didDrag = false;
  function drag() {
    //when the person drags their mouse while holding the mouse button down
    didDrag = true;
    div.innerHTML = "drag"
  }
  function lift() {
    //when the person lifts mouse
    if (!didDrag) {
      //if the person didn't drag
      div.innerHTML = "click";
    } else div.innerHTML = "drag";
    //delete event listeners so that it doesn't keep saying drag
    window.removeEventListener("mousemove", drag)
    window.removeEventListener("mouseup", this)
  }
})
_x000D_
body {
  outline: none;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: Arial, Helvetica, sans-serif;
  overflow: hidden;
}
#div {
  /* calculating -5px for each side of border in case border-box doesn't work */
  width: calc(100vw - 10px);
  height: calc(100vh - 10px);
  border: 5px solid orange;
  background-color: yellow;
  font-weight: 700;
  display: grid;
  place-items: center;
  user-select: none;
  cursor: pointer;
  padding: 0;
  margin: 0;
}
_x000D_
<html>
  <body>
    <div id="div">Click me or drag me.</div>
  </body>
</html>
_x000D_
_x000D_
_x000D_


Using jQuery with a 5 pixel x/y theshold to detect the drag:

var dragging = false;
$("body").on("mousedown", function(e) {
  var x = e.screenX;
  var y = e.screenY;
  dragging = false;
  $("body").on("mousemove", function(e) {
    if (Math.abs(x - e.screenX) > 5 || Math.abs(y - e.screenY) > 5) {
      dragging = true;
    }
  });
});
$("body").on("mouseup", function(e) {
  $("body").off("mousemove");
  console.log(dragging ? "drag" : "click");
});

Pure JS with DeltaX and DeltaY

This DeltaX and DeltaY as suggested by a comment in the accepted answer to avoid the frustrating experience when trying to click and get a drag operation instead due to a one tick mousemove.

    deltaX = deltaY = 2;//px
    var element = document.getElementById('divID');
    element.addEventListener("mousedown", function(e){
        if (typeof InitPageX == 'undefined' && typeof InitPageY == 'undefined') {
            InitPageX = e.pageX;
            InitPageY = e.pageY;
        }

    }, false);
    element.addEventListener("mousemove", function(e){
        if (typeof InitPageX !== 'undefined' && typeof InitPageY !== 'undefined') {
            diffX = e.pageX - InitPageX;
            diffY = e.pageY - InitPageY;
            if (    (diffX > deltaX) || (diffX < -deltaX)
                    || 
                    (diffY > deltaY) || (diffY < -deltaY)   
                    ) {
                console.log("dragging");//dragging event or function goes here.
            }
            else {
                console.log("click");//click event or moving back in delta goes here.
            }
        }
    }, false);
    element.addEventListener("mouseup", function(){
        delete InitPageX;
        delete InitPageY;
    }, false);

   element.addEventListener("click", function(){
        console.log("click");
    }, false);

If just to filter out the drag case, do it like this:

var moved = false;
$(selector)
  .mousedown(function() {moved = false;})
  .mousemove(function() {moved = true;})
  .mouseup(function(event) {
    if (!moved) {
        // clicked without moving mouse
    }
  });

If you want check the click or drag behavior of a specific element you can do this without having to listen to the body.

_x000D_
_x000D_
$(document).ready(function(){_x000D_
  let click;_x000D_
  _x000D_
  $('.owl-carousel').owlCarousel({_x000D_
    items: 1_x000D_
  });_x000D_
  _x000D_
  // prevent clicks when sliding_x000D_
  $('.btn')_x000D_
    .on('mousemove', function(){_x000D_
      click = false;_x000D_
    })_x000D_
    .on('mousedown', function(){_x000D_
      click = true;_x000D_
    });_x000D_
    _x000D_
  // change mouseup listener to '.content' to listen to a wider area. (mouse drag release could happen out of the '.btn' which we have not listent to). Note that the click will trigger if '.btn' mousedown event is triggered above_x000D_
  $('.btn').on('mouseup', function(){_x000D_
    if(click){_x000D_
      $('.result').text('clicked');_x000D_
    } else {_x000D_
      $('.result').text('dragged');_x000D_
    }_x000D_
  });_x000D_
});
_x000D_
.content{_x000D_
  position: relative;_x000D_
  width: 500px;_x000D_
  height: 400px;_x000D_
  background: #f2f2f2;_x000D_
}_x000D_
.slider, .result{_x000D_
  position: relative;_x000D_
  width: 400px;_x000D_
}_x000D_
.slider{_x000D_
  height: 200px;_x000D_
  margin: 0 auto;_x000D_
  top: 30px;_x000D_
}_x000D_
.btn{_x000D_
  display: flex;_x000D_
  align-items: center;_x000D_
  justify-content: center;_x000D_
  text-align: center;_x000D_
  height: 100px;_x000D_
  background: #c66;_x000D_
}_x000D_
.result{_x000D_
  height: 30px;_x000D_
  top: 10px;_x000D_
  text-align: center;_x000D_
}
_x000D_
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>_x000D_
<script src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"></script>_x000D_
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css" />_x000D_
<div class="content">_x000D_
  <div class="slider">_x000D_
    <div class="owl-carousel owl-theme">_x000D_
      <div class="item">_x000D_
        <a href="#" class="btn" draggable="true">click me without moving the mouse</a>_x000D_
      </div>_x000D_
      <div class="item">_x000D_
        <a href="#" class="btn" draggable="true">click me without moving the mouse</a>_x000D_
      </div>_x000D_
    </div>_x000D_
    <div class="result"></div>_x000D_
  </div>_x000D_
  _x000D_
</div>
_x000D_
_x000D_
_x000D_


All these solutions either break on tiny mouse movements, or are overcomplicated.

Here is a simple adaptable solution using two event listeners. Delta is the distance in pixels that you must move horizontally or vertically between the up and down events for the code to classify it as a drag rather than a click. This is because sometimes you will move the mouse or your finger a few pixels before lifting it.

const delta = 6;
let startX;
let startY;

element.addEventListener('mousedown', function (event) {
  startX = event.pageX;
  startY = event.pageY;
});

element.addEventListener('mouseup', function (event) {
  const diffX = Math.abs(event.pageX - startX);
  const diffY = Math.abs(event.pageY - startY);

  if (diffX < delta && diffY < delta) {
    // Click!
  }
});

If you feel like using Rxjs:

_x000D_
_x000D_
var element = document;_x000D_
_x000D_
Rx.Observable_x000D_
  .merge(_x000D_
    Rx.Observable.fromEvent(element, 'mousedown').mapTo(0),_x000D_
    Rx.Observable.fromEvent(element, 'mousemove').mapTo(1)_x000D_
  )_x000D_
  .sample(Rx.Observable.fromEvent(element, 'mouseup'))_x000D_
  .subscribe(flag => {_x000D_
      console.clear();_x000D_
      console.log(flag ? "drag" : "click");_x000D_
  });
_x000D_
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>_x000D_
<script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script>
_x000D_
_x000D_
_x000D_

This is a direct clone of what @wong2 did in his answer, but converted to RxJs.

Also interesting use of sample. The sample operator will take the latest value from the source (the merge of mousedown and mousemove) and emit it when the inner observable (mouseup) emits.


As mrjrdnthms points out in his comment on the accepted answer, this no longer works on Chrome (it always fires the mousemove), I've adapted Gustavo's answer (since I'm using jQuery) to address the Chrome behavior.

var currentPos = [];

$(document).on('mousedown', function (evt) {

   currentPos = [evt.pageX, evt.pageY]

  $(document).on('mousemove', function handler(evt) {

    currentPos=[evt.pageX, evt.pageY];
    $(document).off('mousemove', handler);

  });

  $(document).on('mouseup', function handler(evt) {

    if([evt.pageX, evt.pageY].equals(currentPos))
      console.log("Click")
    else
      console.log("Drag")

    $(document).off('mouseup', handler);

  });

});

The Array.prototype.equals function comes from this answer


It's really this simple

var dragged = false
window.addEventListener('mousedown', function () { dragged = false })
window.addEventListener('mousemove', function () { dragged = true })
window.addEventListener('mouseup', function() {
        if (dragged == true) { return }
        console.log("CLICK!! ")
})

You honestly do not want to add a threshold allowing a small movement. The above is the correct, normal, feel of clicking on all desktop interfaces.

Just try it.

You can easily add an event if you like.


Another solution for class based vanilla JS using a distance threshold

private initDetectDrag(element) {
    let clickOrigin = { x: 0, y: 0 };
    const dragDistanceThreshhold = 20;

    element.addEventListener('mousedown', (event) => {
        this.isDragged = false
        clickOrigin = { x: event.clientX, y: event.clientY };
    });
    element.addEventListener('mousemove', (event) => {
        if (Math.sqrt(Math.pow(clickOrigin.y - event.clientY, 2) + Math.pow(clickOrigin.x - event.clientX, 2)) > dragDistanceThreshhold) {
            this.isDragged = true
        }
    });
}

Add inside the class (SOMESLIDER_ELEMENT can also be document to be global):

private isDragged: boolean;
constructor() {
    this.initDetectDrag(SOMESLIDER_ELEMENT);
    this.doSomeSlideStuff(SOMESLIDER_ELEMENT);
    element.addEventListener('click', (event) => {
        if (!this.sliderIsDragged) {
            console.log('was clicked');
        } else {
            console.log('was dragged, ignore click or handle this');
        }
    }, false);
}

In case you are already using jQuery:

var $body = $('body');
$body.on('mousedown', function (evt) {
  $body.on('mouseup mousemove', function handler(evt) {
    if (evt.type === 'mouseup') {
      // click
    } else {
      // drag
    }
    $body.off('mouseup mousemove', handler);
  });
});

Cleaner ES2015

_x000D_
_x000D_
let drag = false;_x000D_
_x000D_
document.addEventListener('mousedown', () => drag = false);_x000D_
document.addEventListener('mousemove', () => drag = true);_x000D_
document.addEventListener('mouseup', () => console.log(drag ? 'drag' : 'click'));
_x000D_
_x000D_
_x000D_

Didn't experience any bugs, as others comment.


from @Przemek 's answer,

_x000D_
_x000D_
function listenClickOnly(element, callback, threshold=10) {
  let drag = 0;
  element.addEventListener('mousedown', () => drag = 0);
  element.addEventListener('mousemove', () => drag++);
  element.addEventListener('mouseup', e => {
    if (drag<threshold) callback(e);
  });
}

listenClickOnly(
  document,
  () => console.log('click'),
  10
);
_x000D_
_x000D_
_x000D_


For a public action on an OSM map (position a marker on click) the question was: 1) how to determine the duration of mouse down->up (you can't imagine creating a new marker for each click) and 2) did the mouse move during down->up (i.e user is dragging the map).

const map = document.getElementById('map');

map.addEventListener("mousedown", position); 
map.addEventListener("mouseup", calculate);

let posX, posY, endX, endY, t1, t2, action;

function position(e) {

  posX = e.clientX;
  posY = e.clientY;
  t1 = Date.now();

}

function calculate(e) {

  endX = e.clientX;
  endY = e.clientY;
  t2 = (Date.now()-t1)/1000;
  action = 'inactive';

  if( t2 > 0.5 && t2 < 1.5) { // Fixing duration of mouse down->up

      if( Math.abs( posX-endX ) < 5 && Math.abs( posY-endY ) < 5 ) { // 5px error on mouse pos while clicking
         action = 'active';
         // --------> Do something
      }
  }
  console.log('Down = '+posX + ', ' + posY+'\nUp = '+endX + ', ' + endY+ '\nAction = '+ action);    

}