3

I'm trying to detect if the mousedown event is held for a period of time before a mouseup.

I'm using timeout() on an Observable created with fromEvent() to do so, but the timeout returns both Observables.

Below, subscribing to stream returns the event if mousedown is triggered within 1 second, but it also returns 1.

var mousedown = Rx.Observable.fromEvent(target, 'mousedown');
var stream = mousedown.timeout(1000, Rx.Observable.return(1));

var sub = stream.subscribe(
    function (x) { 
        console.log('Next: '+x);
    },
    function (err) {
        console.log('Err: '+err);
    },
    function () {
        console.log('Complete');
    }
);

However, this works as expected:

var source = Rx.Observable.return(42)
    .delay(200)
    .timeout(1000, Rx.Observable.return(1));

I'd like this code to work:

var mousedown = Rx.Observable.fromEvent(target, 'mousedown');
var mouseup = Rx.Observable.fromEvent(target, 'mouseup');

var clickhold = mousedown
.flatMap(function (){
    return mouseup.timeout(1000, Rx.Observable.return('hold'));
})
.filter(function (x) {
    return x === 'hold';
});

clickhold.subscribe(
    function (x) { 
        console.log('Next: '+x);
    },
    function (err) {
        console.log('Err: '+err);
    },
    function () {
        console.log('Complete');
    }
);
Gajus
  • 69,002
  • 70
  • 275
  • 438
Don-Duong Quach
  • 1,046
  • 2
  • 9
  • 11

2 Answers2

6

Instead of using timeout, I used delay and takeUntil:

var target,
    mousedown,
    mouseup;

target = document.querySelector('input');

mousedown = Rx.Observable.fromEvent(target, 'mousedown');
mouseup = Rx.Observable.fromEvent(target, 'mouseup');

var clickhold = mousedown
    .flatMap(function(){
        // Triggered instantly after mousedown event.
        return Rx.Observable
            .return('hold')
            .delay(1000)
            // Discards the return value if by the time .delay() is complete
            // mouseup event has been already completed.
            .takeUntil(mouseup);
    });

clickhold.subscribe(
        function (x) { 
            console.log('Next: ' + x);
        },
        function (err) {
            console.log('Err: ' + err);
        },
        function () {
           console.log('Complete');
        }
    );
<script src='https://rawgit.com/Reactive-Extensions/RxJS/v.2.5.3/dist/rx.all.js'></script>

<input type='button' value='Button' />
Gajus
  • 69,002
  • 70
  • 275
  • 438
Don-Duong Quach
  • 1,046
  • 2
  • 9
  • 11
1

You came up with a great solution on your own. Here's what I would change:

  1. Move the inner observable (timer(...).takeUntil(...).select(...)) out of flatMap, so it isn't re-allocated for each mouse down.

You've got the rest right. For my usage, I usually retain the original mousedown event and use that instead of 'hold'. That requires returnValue and delay instead of timer and select.

var target,
    mousedown,
    mouseup;

target = document.querySelector('input');

mousedown = Rx.Observable.fromEvent(target, 'mousedown');
mouseup = Rx.Observable.fromEvent(target, 'mouseup');

var clickhold = mousedown
    .flatMap(function (e) {
        return Rx.Observable
            .return(e)
            .delay(1000)
            .takeUntil(mouseup);
    });

clickhold.subscribe(function (x) { 
        console.log('onNext: ', x);
    });
<script src='https://rawgit.com/Reactive-Extensions/RxJS/v.2.5.3/dist/rx.all.js'></script>

<input type='button' value='Button' />

Or, for a completely different approach...

var Observable  = Rx.Observable,
    fromEvent   = Observable.fromEvent.bind(Observable, target),
    holdDelay   = Observable.empty().delay(1000);

Observable
  .merge(
    [
      fromEvent('mouseup')
        .map(empty),
      fromEvent('mousedown')
        .map(Observable.returnValue)
        .map(holdDelay.concat.bind(holdDelay))
    ]
  )
  .switchLatest();

Ok so that's weird. I'm really just giving it as food for though, and to show off that this can be done in a number of different ways.

Gajus
  • 69,002
  • 70
  • 275
  • 438
cwharris
  • 17,835
  • 4
  • 44
  • 64
  • Thanks! I was thinking about using switchLatest as well, and your example makes it clear how I can use it. – Don-Duong Quach Jun 02 '14 at 17:02
  • As a follow up how would you check for the mouseup after the 'clickhold'? I'm subscribing on Observable.when(clickhold.and(mouseup).then(function(){ return 'release';}), mouseup.then(function(){ return 'mouseup';}), to reverse state. – Don-Duong Quach Jun 02 '14 at 17:20
  • I'm a bit confused by what you're asking. Phrasing such as "check for" works out really well when talking about syncronous pull-based systems, but I have a hard time understanding it when we're talking about events. Can you rephrase? – cwharris Jun 02 '14 at 20:14
  • Basically I want to check if the 'clickhold' sequence is followed by a mouseup. For example if I click on a box and change its color, then on mouseup I'd like to change the color back. I have a solution here that uses when(): http://codepen.io/geekrelief/pen/FAimL?editors=101 But I wonder if there's a less verbose way of doing it. – Don-Duong Quach Jun 03 '14 at 04:17
  • You might be interested in Paul Taylor's unified gestures library for RxJS. https://github.com/trxcllnt/rxjs-gestures – cwharris Jun 03 '14 at 05:28
  • Thanks! I'll definitely look into it! – Don-Duong Quach Jun 03 '14 at 17:34