7

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!

Achrome
  • 7,273
  • 14
  • 32
  • 44
Jesse Hattabaugh
  • 7,359
  • 8
  • 30
  • 35
  • 2
    One thing that you should look to avoid: adding subscriptions inside onValue handlers like you do here. It is an anti-pattern, the correct way to do this is to use a combinator (here it's `flatMap`) – OlliM Jun 22 '15 at 07:12

2 Answers2

16

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>
paulpdaniels
  • 16,958
  • 2
  • 46
  • 51
1
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';
    });
MoLow
  • 2,990
  • 2
  • 17
  • 39