217

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?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Leem
  • 17,220
  • 36
  • 109
  • 159

20 Answers20

233

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)
connexo
  • 53,704
  • 14
  • 91
  • 128
wong2
  • 34,358
  • 48
  • 134
  • 179
  • 55
    Just remember to require a minimum delta X or Y on mousemove to trigger a drag. Would be frustrating to try to click and get a drag operation instead due to a one tick mousemove – Erik Rydgren May 18 '11 at 09:14
  • it can be just a mousemove if flag == 1 right? no neccessary drag. – yeeen May 31 '12 at 05:49
  • @yeeen then there will be no `mouseup` – wong2 May 31 '12 at 12:11
  • 13
    I don't think this works anymore in the lastest chrome: 32.0.1700.72 Mousemove fires whether you move mouse or not – mrjrdnthms Jan 11 '14 at 00:38
  • 27
    This accepted answer code should include a minimum delta condition between XY mouse coordinates at `mousedown` and `mouseup` instead of listening to the `mousemove` event to set a flag. Moreover, it would fix the issue mentioned by @mrjrdnthms – Billybobbonnet Oct 20 '15 at 17:22
  • 3
    I'm running Chrome 56.0.2924.87 (64-bit) and I'm not experiencing the issues @mrjrdnthms is describing. – jkupczak Mar 15 '17 at 19:38
  • It has a little bug. If I move mouse over the `xxxx` element, `flag` get `1` and never turn to `0` , so never see the `click`. – AmerllicA Aug 05 '17 at 12:01
  • 1
    @AmerllicA this is probably not a bug but expected behavior, however you could watch the mouseenter and mouseleave events if that's interesting to your use case – Rivenfall Jun 26 '19 at 12:08
  • No need to use `mousedown`/`mouseup` events anymore, you can just use the native drag events - https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API –  Mar 30 '20 at 23:30
  • 1
    @Catalin it's still needed for custom interactions like panning a map / graph / image carousel. The drag events don't have x/y information; they're analogous to OS-level text / file drag-and-drop; i.e. for data transfer. – Matthias Apr 02 '20 at 14:55
  • Using `#element::-webkit-scrollbar { display: none; }` remove scroll and works perfect! Thanks. – GIA Sep 20 '21 at 16:12
71

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!
  }
});
andreyrd
  • 991
  • 7
  • 9
  • 2
    By far the best answer! – Giorgio Tempesta Mar 04 '20 at 10:06
  • Hi @andreyrd, may i know what `delta` is used for this? it is something to do with tap in mobile device? – Azriz Apr 18 '20 at 11:07
  • 2
    @Haziq I think as people mentioned in comments of top solutions `delta` is used for "Would be frustrating to try to click and get a drag operation instead due to a one tick mousemove" – Mykhailo Bykhovtsev Jul 16 '20 at 19:28
  • 1
    I updated the answer with an explanation. Basically if your finger less than 6 pixels, it will still count as a click. If it moves 6 or more pixels, it will count as a drag. – andreyrd Jul 17 '20 at 20:30
  • 1
    Nice. Be aware that this doesn't serve properly in _some_ cases. **One example**: drag&dropping. Dragging something away but coming back with it because user changed his mind can have undesired click as result. In such a case, a threshold delta must be checked on `mousemove` as some answers here suggest. – liviriniu Feb 14 '21 at 14:56
  • 4
    This solution considers "moving around however far and long while holding button pressed and then releasing it right where you started from" as also a click. – liviriniu Feb 14 '21 at 15:04
55

Cleaner ES2015

let drag = false;

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

Didn't experience any bugs, as others comment.

vsync
  • 118,978
  • 58
  • 307
  • 400
Przemek
  • 3,855
  • 2
  • 25
  • 33
38

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);
  });
});
Gustavo Rodrigues
  • 3,517
  • 1
  • 25
  • 21
  • 1
    Even if you move the mouse a tiny bit while clicking, this will say `drag`. An extra scope like other comments are saying may be needed here. – ChiMo Oct 18 '16 at 05:37
  • 1
    @ChiMo What I'm been using is storing mouse position from the first `evt` and comparing with the position of the second `evt`, so, for example: `if (evt.type === 'mouseup' || Math.abs(evt1.pageX - evt2.pageX) < 5 || Math.abs(evt1.pageY - evt2.pageY) < 5) { ...`. – Gustavo Rodrigues Oct 19 '16 at 10:01
  • 1
    I tried all other answers to this question, and this is the only one that worked when checking for `.on('mouseup mousemove touchend touchmove')`, and on top of that doesn't set position variables. Great solution! – TheThirdMan Feb 24 '17 at 15:56
  • Sometimes when I clicked on an element the "evt.type" return "mousemove" instead on mouseup. How can I resolve that issue? – Libu Mathew Oct 09 '17 at 06:01
18

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).

nirvana-msu
  • 3,877
  • 2
  • 19
  • 28
14

If you feel like using Rxjs:

var element = document;

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

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.

vsync
  • 118,978
  • 58
  • 307
  • 400
Dorus
  • 7,276
  • 1
  • 30
  • 36
11

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

Community
  • 1
  • 1
Francisco Aquino
  • 9,097
  • 1
  • 31
  • 37
5

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");
});
silverwind
  • 3,296
  • 29
  • 31
5

You could do this:

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)
  }
})
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;
}
<html>
  <body>
    <div id="div">Click me or drag me.</div>
  </body>
</html>
Zayan
  • 49
  • 1
  • 3
  • I took your approach and made a [React version](https://stackoverflow.com/a/64766667/2891356) for anyone interested. – Mike K Nov 10 '20 at 09:59
5

Had the same problem recently with a tree list where the user can either click on the item or drag it, made this small Pointer class and put it in my utils.js

function Pointer(threshold = 10) {
  let x = 0;
  let y = 0;

  return {
    start(e) {
     x = e.clientX;
     y = e.clientY;
    },

    isClick(e) {
      const deltaX = Math.abs(e.clientX - x);
      const deltaY = Math.abs(e.clientY - y);
      return deltaX < threshold && deltaY < threshold;
    }
  }
}

Here you can see it at work:

function Pointer(threshold = 10) {
  let x = 0;
  let y = 0;

  return {
    start(e) {
     x = e.clientX;
     y = e.clientY;
    },

    isClick(e) {
      const deltaX = Math.abs(e.clientX - x);
      const deltaY = Math.abs(e.clientY - y);
      return deltaX < threshold && deltaY < threshold;
    }
  }
}

const pointer = new Pointer();

window.addEventListener('mousedown', (e) => pointer.start(e))
//window.addEventListener('mousemove', (e) => pointer.last(e))
window.addEventListener('mouseup', (e) => {
  const operation = pointer.isClick(e) 
    ? "Click"
    : "Drag"
  console.log(operation)
})
Fennec
  • 1,535
  • 11
  • 26
4

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.

Luc
  • 5,339
  • 2
  • 48
  • 48
Fattie
  • 27,874
  • 70
  • 431
  • 719
3

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
    }
  });
seantomburke
  • 10,514
  • 3
  • 18
  • 23
jqgsninimo
  • 6,562
  • 1
  • 36
  • 30
3

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);
}
Tim Rasim
  • 655
  • 7
  • 20
1

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);
Syed Waqas Bukhary
  • 5,130
  • 5
  • 47
  • 59
1

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);    

}
Wolden
  • 195
  • 1
  • 12
1

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} />
    );
})
Mike K
  • 7,621
  • 14
  • 60
  • 120
0

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

$(document).ready(function(){
  let click;
  
  $('.owl-carousel').owlCarousel({
    items: 1
  });
  
  // prevent clicks when sliding
  $('.btn')
    .on('mousemove', function(){
      click = false;
    })
    .on('mousedown', function(){
      click = true;
    });
    
  // 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
  $('.btn').on('mouseup', function(){
    if(click){
      $('.result').text('clicked');
    } else {
      $('.result').text('dragged');
    }
  });
});
.content{
  position: relative;
  width: 500px;
  height: 400px;
  background: #f2f2f2;
}
.slider, .result{
  position: relative;
  width: 400px;
}
.slider{
  height: 200px;
  margin: 0 auto;
  top: 30px;
}
.btn{
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  height: 100px;
  background: #c66;
}
.result{
  height: 30px;
  top: 10px;
  text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/owl.carousel.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/OwlCarousel2/2.3.4/assets/owl.carousel.min.css" />
<div class="content">
  <div class="slider">
    <div class="owl-carousel owl-theme">
      <div class="item">
        <a href="#" class="btn" draggable="true">click me without moving the mouse</a>
      </div>
      <div class="item">
        <a href="#" class="btn" draggable="true">click me without moving the mouse</a>
      </div>
    </div>
    <div class="result"></div>
  </div>
  
</div>
Lasithds
  • 2,161
  • 25
  • 39
0

from @Przemek 's answer,

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
);
Jehong Ahn
  • 1,872
  • 1
  • 19
  • 25
0

The following coding is to detect the movement of mouseup and mousedown.

It shall work for most of the cases. It also depends on how you treat mouseevent as Click.

In JavaScript, the detection is very simple. It does not concern how long you press or movement between the mousedown and mouseup. Event.detail would not reset to 1 when your mouse is moved between the mousedown and mouseup.

If you need to differentiate the click and long press, you need to check the difference in event.timeStamp too.

// ==== add the code at the begining of your coding ====
let clickStatus = 0;
(() => {
    let screenX, screenY;
    document.addEventListener('mousedown', (event) => ({screenX, screenY} = event), true); 
    document.addEventListener('mouseup', (event) => (clickStatus = Math.abs(event.screenX - screenX) + Math.abs(event.screenY - screenY) < 3), true);
})();
// ==== add the code at the begining of your coding ====

$("#draggable").click(function(event) {
    if (clickStatus) {
        console.log(`click event is valid, click count: ${event.detail}`)
    } else {
        console.log(`click event is invalid`)
    }

})
<!doctype html>
<html lang="en">
<!-- coding example from https://jqueryui.com/draggable/ -->
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>jQuery UI Draggable - Default functionality</title>
  <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
  <link rel="stylesheet" href="/resources/demos/style.css">
  <style>
  #draggable { width: 150px; height: 150px; padding: 0.5em; }
  </style>
  <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
  <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
  <script>
  $( function() {
    $( "#draggable" ).draggable();
  } );
  </script>
</head>
<body>
 
<div id="draggable" class="ui-widget-content">
  <p>Drag me around</p>
</div>
 
 
</body>
</html>
Chester Fung
  • 182
  • 1
  • 10
0

Is very easy,

el = document.getElementById("your_id");
var isDown = false;
el.addEventListener('mousedown', function () {
    isDown = true;
});
el.addEventListener('mouseup', function () {
    isDown = false;
});
el.addEventListener('mousemove', function () {
    if (isDown) {
        // your code goes here
    }
});
Hardip M
  • 1
  • 1