How to handle mouse and touch events simultaneously with reactive event streams

3.5k Views Asked by At

I'm building an audio playback control that lets users scrub back and forth through an audio file. It needs to work with touch and mouse events. How should I go about managing the events for this with reactive event streams?

Here's a rough idea of how I would expect to build it.

<div id="timeline">
  <span id="scrubber"></span>
</div>

then, using Bacon.js to create event streams

var mousedowns = $('#timeline').asEventStream('mousedown');
var touchstarts = $('#timeline').asEventStream('touchstart');

var starts = Bacon.mergeAll(mousedowns, touchstarts);

var mousemoves = $('#timeline').asEventStream('mousemove');
var touchmoves = $('#timeline').asEventStream('touchmove');

var moves = Bacon.mergeAll(mousemoves, touchmoves);

var mouseups = $('#timeline').asEventStream('mouseup');
var touchends = $('#timeline').asEventStream('touchend');

var ends = Bacon.mergeAll(mouseups, touchends);

starts.onValue(function () {
  var repositionScrubber = moves.onValue(function (ev) {
    $('#scrubber').moveTo(ev.offsetX);
  });
  ends.onValue(function () {
    repositionScrubber.stop();
  });
});

I'm sure that's all sorts of wrong, but I'm really new to handling events with observable streams and I don't know of any good cookbooks for it yet. Any help will be appreciated!

2

There are 2 best solutions below

0
On

This is essentially the canonical drag and drop recipe.

The minimum working example in RxJS is something like this:

var $timeline = $('#timeline');
var $scrubber = $('#scrubber');

var mouseDown = Rx.Observable.merge(
  Rx.Observable.fromEvent($timeline, 'mousedown'),
  Rx.Observable.fromEvent($timeline, 'touchstart'));

var mouseUp = Rx.Observable.merge(
  Rx.Observable.fromEvent($timeline, 'mouseup'),
  Rx.Observable.fromEvent($timeline, 'touchend'));

var mouseMove = Rx.Observable.merge(
  Rx.Observable.fromEvent($timeline, 'mousemove'),
  Rx.Observable.fromEvent($timeline, 'touchmove'));

var subscription = mouseDown.flatMapLatest(function(md) {

  // calculate offsets when mouse down
  var startX = md.offsetX;

  return mouseMove.takeUntil(mouseUp)
                  .map(function(mm) {
                       mm.preventDefault();

                       return {
                        left: mm.clientX - startX,
                       };
                  });
})
.subscribe(function(e) {
  $scrubber.css(e);
});

var $timeline = $('#timeline');
var $scrubber = $('#scrubber');
    
    var mouseDown = Rx.Observable.merge(
      Rx.Observable.fromEvent($timeline, 'mousedown'),
      Rx.Observable.fromEvent($timeline, 'touchstart'));
    
    var mouseUp = Rx.Observable.merge(
      Rx.Observable.fromEvent($timeline, 'mouseup'),
      Rx.Observable.fromEvent($timeline, 'touchend'));
    
    var mouseMove = Rx.Observable.merge(
      Rx.Observable.fromEvent($timeline, 'mousemove'),
      Rx.Observable.fromEvent($timeline, 'touchmove'));
    
    var subscription = mouseDown.flatMapLatest(function(md) {
      
      // calculate offsets when mouse down
      var startX = md.offsetX;
      
      return mouseMove.takeUntil(mouseUp)
                      .map(function(mm) {
                           mm.preventDefault();

                           return {
                            left: mm.clientX - startX,
                           };
                      });
    })
    .subscribe(function(e) {
      $scrubber.css(e);
    });
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/2.5.3/rx.all.js"></script>
<div id="timeline" style="height: 100px; width: 100px; background: yellow; position: absolute;">
  <span id="scrubber" style="height: 20px; width: 30px; background: green; position: relative;">Foo</span>
</div>

0
On
var mousemove = Rx.Observable.merge(
            Rx.Observable.fromEvent(document, 'mousemove')
                .map((e) => { e.preventDefault(); return e; }),
            Rx.Observable.fromEvent(document, 'touchmove'))
                .map((e) => e.touches[0]),
        mouseup = Rx.Observable.merge(
            Rx.Observable.fromEvent(dragTarget, 'mouseup'),
            Rx.Observable.fromEvent(dragTarget, 'touchend')),
        mousedown = Rx.Observable.merge(
            Rx.Observable.fromEvent(dragTarget, 'mousedown'),
            Rx.Observable.fromEvent(dragTarget, 'touchstart')
                .map((e) => {
                    var rect = e.target.getBoundingClientRect();
                    e.offsetX = e.touches[0].pageX - rect.left;
                    e.offsetY = e.touches[0].pageY - rect.top;
                    return e;
                }));

mousedown
    .flatMap((start) => mousemove
        .map((mm) =>({
            left: mm.clientX - start.offsetX,
            top: mm.clientY - start.offsetY
        }))
        .takeUntil(mouseup))
    .subscribe(function (pos) {
        dragTarget.style.top = pos.top + 'px';
        dragTarget.style.left = pos.left + 'px';
    });