Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RxJS: distinguish single click from drag

I have an AngularJS component that should react to either a single click or a drag (resizing an area).

I started to use RxJS (ReactiveX) in my application, so I try to find a solution using it. The Angular side of the request is minor...

To simplify the problem (and to train myself), I made a slider directive, based on the rx.angular.js drag'n'drop example: http://plnkr.co/edit/UqdyB2 See the Slide.js file (the other code is for other experiments). The code of this logic is:

    function(scope, element, attributes)
    {
      var thumb = element.children(0);
      var sliderPosition = element[0].getBoundingClientRect().left;
      var sliderWidth = element[0].getBoundingClientRect().width;
      var thumbPosition = thumb[0].getBoundingClientRect().left;
      var thumbWidth = thumb[0].getBoundingClientRect().width;

      // Based on drag'n'drop example of rx-angular.js
      // Get the three major events
      var mousedown = rx.Observable.fromEvent(thumb,     'mousedown');
      var mousemove = rx.Observable.fromEvent(element,   'mousemove');
      var mouseup   = rx.Observable.fromEvent($document, 'mouseup');

      // I would like to be able to detect a single click vs. click and drag.
      // I would say if we get mouseup shortly after mousedown, it is a single click;
      // mousedown.delay(200).takeUntil(mouseup)
      //   .subscribe(function() { console.log('Simple click'); }, undefined, function() { console.log('Simple click completed'); });

      var locatedMouseDown = mousedown.map(function (event) 
      {
        event.preventDefault();
        // console.log('Click', event.clientX - sliderPosition);
        // calculate offsets when mouse down
        var initialThumbPosition = thumb[0].getBoundingClientRect().left - sliderPosition;
        return { from: initialThumbPosition, offset: event.clientX - sliderPosition };
      });

      // Combine mouse down with mouse move until mouse up
      var mousedrag = locatedMouseDown.flatMap(function (clickInfo) 
      {
        return mousemove.map(function (event)
        {
          var move = event.clientX - sliderPosition - clickInfo.offset;
          // console.log('Move', clickInfo);
          // calculate offsets from mouse down to mouse moves
          return clickInfo.from + move;
        }).takeUntil(mouseup);
      });

      mousedrag 
        .map(function (position)
        {
          if (position < 0)
            return 0;
          if (position > sliderWidth - thumbWidth)
            return sliderWidth - thumbWidth;
          return position;
        })
        .subscribe(function (position) 
        {
          // console.log('Drag', position);
          // Update position
          thumb.css({ left: position + 'px' });
        });
    }

That's mostly D'n'D constrained horizontally and to a given range.

Now, I would like to listen to mousedown, and if mouse up happens within a short while (say 200 ms, to adjust), I see it as a click and I do a specific treatment (eg. resetting the position to zero).

I tried with delay().takeUntil(mouseup), as seen in another SO answer, without success. Perhaps a switch() might be needed, too (to avoid going the drag route).

Any idea? Thanks in advance.

like image 412
PhiLho Avatar asked Oct 23 '15 16:10

PhiLho


People also ask

How to distinguish between drag and click?

The basic difference between a click and a drag event is the mouse movement. The mouse event that differentiates a click and drag event is the “mouse move” event. In a “click” event, there is no “mouse move” event. However, the “mouse down” and “mouse up” event remains the same for both click and drag.


2 Answers

You can use timeout (timeoutWith if you are using ReactiveX/RxJS)

var click$ = mousedown.flatMap(function (md) {
  return mouseup.timeoutWith(200, Observable.empty());
});

If the mouseup doesn't occur before the timeout it will just propagate an empty Observable instead. If it does then the downstream observer will receive an event.

like image 161
paulpdaniels Avatar answered Nov 15 '22 10:11

paulpdaniels


Isn't the trick with delay(Xms).takeUntil(mouseup) doing the opposite of what you want? I mean, you want to detect when the mouseup event happens before the countdown, while the aformentioned trick detect when the mouseup event happens after.

I would try something around those lines (untested for now, but hopefully it will orient you in some positive direction):

var click$ = mousedown.flatMap(function ( mouseDownEv ) {
  return merge(
      Rx.just(mouseDownEv).delay(Xms).map(function ( x ) {return {event : 'noclick'};}),
      mouseup.map(function ( mouseUpEv ) {return {event : mouseUpEv};})
      ).first();
});

The idea is to race the mouseup event against a dummy emission happening after your delay, and see who wins. So if click$ emits 'noclick' then you can consider that no click happened.

Hopefully that works, i will test soon but if you do before me, let me know.

like image 42
user3743222 Avatar answered Nov 15 '22 12:11

user3743222