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!
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!
Share Improve this question edited Jun 21, 2015 at 21:13 Achrome 7,82114 gold badges38 silver badges45 bronze badges asked Jun 21, 2015 at 21:07 Jesse HattabaughJesse Hattabaugh 7,9668 gold badges36 silver badges39 bronze badges 1-
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 binator (here it's
flatMap
) – OlliM Commented Jun 22, 2015 at 7:12
2 Answers
Reset to default 18This 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./ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare./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>
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';
});